Compare commits

...

125 Commits

Author SHA1 Message Date
mhsanaei
2ffde55f8f v2.5.6 - Happy Nowruz 2025-03-20 10:54:54 +01:00
Shishkevich D.
6e5ed881f2 chore: pretty Inbounds page (#2791)
* chore: pretty 'Inbounds' page

* chore: return styles for aCustomStatistic

styles was intended to properly display a-statistic in the app, but for some unknown reason it was removed

* fix: switch style in dark mode

---------
2025-03-18 22:13:01 +01:00
Kirill Dunaev
d52c50fd9e Russian translation fixes (#2792) 2025-03-18 20:56:58 +07:00
somebodywashere
fa5fb927c1 Update to regular cert issue (#2790) 2025-03-18 13:47:58 +01:00
mhsanaei
f7198c4c2f Update index.html 2025-03-18 13:39:07 +01:00
Tara Rostami
21e7d45b54 Fixes and improvements (#2789)
* Fixes and improvements

* Update translate.en_US.toml
2025-03-18 09:51:05 +01:00
Shishkevich D.
db62a07fb8 Code refactoring (#2785)
* chore: pretty theme menu in sidebar

* refactor: renaming component templates

* refactor: create custom `a-statistic` component

* fix: display button text only on large screens

* chore: remove loading background in overview page

* fix: show `Version` text when xray version is unknown
2025-03-17 18:26:07 +07:00
somebodywashere
e3120c4028 Updates to CF cert issue (#2780) 2025-03-17 09:12:52 +01:00
Shishkevich D.
7ae855e7c9 chore: some improvements (#2782)
* chore: improve outbound link input

* chore: ui improvement
2025-03-17 08:26:59 +07:00
Shishkevich D.
b9307c6c9c chore: pretty 'Overview' page (#2772)
* chore: pretty 'Overview' page

* chore: some improvements in 'overview page'
- reduced font size
- added caption to buttons
- fixed display of xray state
- xray version display returned
2025-03-15 12:15:46 +01:00
Ilya Afanasov
d30cdbf49a feat: custom subscription title in panel (#2773)
* feat: custom subscription title in panel

* feat: added translations
2025-03-15 08:16:59 +01:00
Sanaei
cac00224db runs-on: ubuntu-22.04 (#2767)
https://github.com/actions/runner-images/issues/11101
2025-03-13 16:06:08 +01:00
mhsanaei
b68f0a206c xray log - minor change 2025-03-13 11:48:00 +01:00
mhsanaei
0bde51b91e Refactor: Use any instead of interface{} 2025-03-12 20:43:43 +01:00
mhsanaei
280a22b57d warp - optimize utility code 2025-03-12 19:27:19 +01:00
mhsanaei
315d852087 fix - public IP #2763 2025-03-12 19:20:13 +01:00
mhsanaei
6a0d2e0a29 Axios v1.8.2 2025-03-11 13:18:52 +01:00
Shishkevich D.
a811225610 fix: protocol checking during random uuidv4 generation
fixes the https://github.com/MHSanaei/3x-ui/issues/2750 issue
2025-03-10 22:09:51 +07:00
mhsanaei
1893c3814d v2.5.5 2025-03-10 14:33:58 +01:00
mhsanaei
422c391f96 Xray log: show failed on error log level 2025-03-10 14:31:06 +01:00
mhsanaei
f7f95ffbae bug fix - xray log 2025-03-10 13:46:46 +01:00
mhsanaei
f408bd7c77 Xray core v2.3.6 + update dependencies 2025-03-10 09:51:31 +01:00
Shishkevich D.
ad13ce6cde fix: generating shortIds for vless reality (#2745) 2025-03-09 19:37:53 +07:00
Shishkevich D.
c35179d924 chore: remove unused variable 2025-03-09 06:38:45 +00:00
Shishkevich D.
cedc7f0fb8 chore: refactoring RandomUtil class
now we use window.crypto.getRandomValues to generate random values.
2025-03-09 06:37:05 +00:00
Shishkevich D.
64fa0e97a3 chore: use crypto.randomUUID() for generating UUIDv4 2025-03-09 06:09:42 +00:00
Shishkevich D.
a45e9de472 chore: use Base64 library for generating SS password 2025-03-09 06:06:27 +00:00
Shishkevich D.
101e9ebf35 fix: modals style 2025-03-09 06:01:27 +00:00
Shishkevich D.
17a76d2843 Revert "chore: add missing params for grpc stream settings (outbound)"
This reverts commit 1c59afe031.
2025-03-09 05:38:34 +00:00
Shishkevich D.
a23a5de540 Revert "chore: add new grpc params for outbound (#2744)"
This reverts commit c49ec9a74c.
2025-03-09 05:37:50 +00:00
Shishkevich D.
a16e83468b chore: add dns type for kcp protocol
see https://xtls.github.io/config/transports/mkcp.html#headerobject
2025-03-09 12:23:35 +07:00
Shishkevich D.
1c59afe031 chore: add missing params for grpc stream settings (outbound) 2025-03-09 04:49:17 +00:00
Shishkevich D.
c49ec9a74c chore: add new grpc params for outbound (#2744) 2025-03-09 11:28:12 +07:00
mhsanaei
a0dd101d97 tgbot - restart
change restart force to restart
2025-03-08 23:08:04 +01:00
mhsanaei
700cf9c10b minor changes 2025-03-08 18:14:48 +01:00
Shishkevich D.
697cd5e6d9 Code refactoring (#2739)
* refactor: switching to the use of typed props

* refactor: `password-input` -> `a-password-input`

* fix: qr modal copy error
2025-03-08 22:41:27 +07:00
Shishkevich D.
c6d27a4463 chore: pretty backup and xray version modal (#2737)
* chore: pretty `backup & restore` modal

* chore: pretty `xray version` modal

* fix: new `xray version` modal style
2025-03-08 20:41:23 +07:00
Shishkevich D.
751f564c4a fix: base64 encoding on vmess/shadowsocks inbounds (#2736) 2025-03-08 12:33:34 +07:00
Shishkevich D.
6658f648e6 refactor: move language manager to utils (#2735) 2025-03-08 09:54:41 +07:00
Shishkevich D.
d6f9f3f6d3 chore: add empty screens for empty data (balancers, reverses, dns) 2025-03-07 22:17:14 +07:00
Shishkevich D.
fad6c497eb chore: add empty screens for empty data (balancers, reverses, dns)
cleaned up some of the margins & paddings
2025-03-07 13:56:03 +00:00
Sanaei
42fa64770b Merge pull request #2732 from shishkevichd/refactor/refactor-1
Code refactoring
2025-03-07 12:40:35 +01:00
Shishkevich D.
0a207b8a2c refactor: merging all util functions into classes 2025-03-07 09:07:23 +00:00
Shishkevich D.
26bf693dbd refactor: move copy function to utils.js 2025-03-07 07:27:33 +00:00
Shishkevich D.
7483fb2ec5 refactor: delete base64js
instead of base64 library you can use built-in JS functions `btoa()` and `atob()`
2025-03-07 07:11:03 +00:00
Shishkevich D.
2d8cca3a2e refactor: delete clipboardjs (#2727)
text copying can be done without using additional libraries
2025-03-06 20:43:46 +01:00
UltraMegaPotato
cf7fec1351 Change the cpu usage interval from second to minute (#2729)
feature(check_cpu_usage): Change the usage interval from second to minute
2025-03-06 20:38:58 +01:00
mhsanaei
361849b9db Balancer fallbackTag #2724 2025-03-06 20:35:17 +01:00
Shishkevich D.
c13db7922e Pretty Panel and Xray settings (#2726)
* chore: refactor `setting-list-item` component

* chore: remove padding

* chore: replace settings list with settings collapse panels

* chore: add missing translations

* chore: fix translation
2025-03-06 11:17:25 +01:00
mhsanaei
d6d05a9b4d v2.5.4 2025-03-05 15:42:51 +01:00
mhsanaei
3caace2cb6 update dependencies 2025-03-05 15:40:27 +01:00
Shishkevich D.
99f26be30d feat: add statistics section (#2718)
the "Outbounds Traffic" parameter, which was misleading, was also renamed and moved
2025-03-05 13:27:25 +01:00
mhsanaei
b0edd24c52 Noise: Add hex 2025-03-05 13:22:07 +01:00
mhsanaei
f0cfd48f66 Sockopt: Add addressPortStrategy 2025-03-05 13:14:17 +01:00
Shishkevich D.
f5aea03765 chore: add global params for DNS (#2713)
parameter list: `disableCache`, `disableFallback`, `disableFallbackIfMatch`, `clientIp`
2025-03-04 14:18:51 +01:00
mhsanaei
14cdde371f better view for uTLS 2025-03-04 12:50:49 +01:00
mhsanaei
99a23f25d5 TLS-REALITY : hide value of private key 2025-03-04 10:36:02 +01:00
Shishkevich D.
653ec90451 chore: pretty dns section in xray settings (#2700) 2025-03-04 09:55:13 +01:00
Shishkevich D.
91a84db479 chore: pretty auth tab in panel settings (#2701) 2025-03-04 09:54:59 +01:00
mhsanaei
0f97eca314 Xray core v25.3.3 2025-03-04 09:54:10 +01:00
mhsanaei
fb79081aa1 TLS fingerprints: randomizednoalpn 2025-03-03 10:20:52 +01:00
mhsanaei
ea19fb8ff6 fix mistake 2025-03-01 10:19:52 +01:00
atarwn
f4cfe9eb63 Improved ru_RU translation (#2677)
* Improved ru_RU translation

Some words have been changed to sound nicer. I made some words less complicated to understand. I also fixed some warnings and messages so they are easier to read.

* Apply suggestions from code review
2025-02-28 10:42:27 +01:00
mhsanaei
4def70a006 Xray core buggy version removed 2025-02-27 13:40:56 +01:00
mhsanaei
e0e9e2681a docker: go 1.24 2025-02-25 18:56:35 +01:00
mhsanaei
31e1581d6b v2.5.3 2025-02-25 18:43:29 +01:00
mhsanaei
018e98a510 Go v1.24.0 2025-02-25 18:43:15 +01:00
mhsanaei
21ea673c30 Make wget verify certificates part2 #2661
Co-Authored-By: İrem Kuyucu <siren@kernal.eu>
2025-02-24 13:15:18 +01:00
Shishkevich D.
08b55da408 feat: add quic protocol in xray rule modal (#2666) 2025-02-24 09:23:59 +01:00
atarwn
7a3ee69a7f Virtuozzo linux support (#2668) 2025-02-24 09:22:34 +01:00
mhsanaei
664bd9b596 bug fix #2660 2025-02-22 14:31:08 +01:00
mhsanaei
ceb1217121 serverNameToVerify to verifyPeerCertInNames #2662 2025-02-22 14:09:52 +01:00
mhsanaei
e754523689 Xray core v25.2.21 2025-02-22 13:46:15 +01:00
İrem Kuyucu
e84503feec Make wget verify certificates (#2661) 2025-02-22 11:53:36 +01:00
AAA
1bbf31df9f feat(externalTrafficJob): External Traffic Inform (#2660)
* Add Setting entity + GUI field in panel settings

* Add a missing 'Traffic' in InformEnabale field

* Add ExternalTrafficURL Post request call

* Add translation + cleanup

* Move options to General tab

---------

Co-authored-by: root <root@vm3562019.stark-industries.solutions>
Co-authored-by: root <root@vm3688062.stark-industries.solutions>
2025-02-22 10:45:14 +01:00
mhsanaei
49bfff9fa5 v2.5.2 2025-02-04 11:38:57 +01:00
Sanaei
d18a1a37ce revert group management (#2656)
* Revert "json post base path bug fixed (#2647)"

This reverts commit 04cf250a54.

* Revert "Group Management of Subscription Clients"

* Revert "fix getSubGroupClients for enable/disable and edit clients."

* Revert "Enhance database initialization in db.go (#2645)"

This reverts commit 66fe84181b.

* Revert "Add checkpoint handling in CloseDB function (#2646)"

This reverts commit 4dd40f6f19.

* Revert "Improved database model migration and added indexing (#2655)"

This reverts commit b922d986d6.
2025-02-04 11:27:58 +01:00
mhsanaei
04c6b2722b README: Persian 2025-02-03 20:30:42 +01:00
mhsanaei
94d651fc93 v2.5.1 2025-02-03 17:40:33 +01:00
mhsanaei
aae0cb37b7 Xray Core v25.1.30 2025-02-03 17:37:01 +01:00
Zahar Izmailov
b922d986d6 Improved database model migration and added indexing (#2655) 2025-02-03 13:36:03 +01:00
Abolfazl Fazilat
8a7cffd63f Completed translation for missing text (#2653) 2025-01-31 18:54:52 +01:00
Sanaei
c8e8c97afc Merge pull request #2652 from Incognito-Coder/main
Some Improvement
2025-01-31 16:01:14 +01:00
AghayeCoder
46ba4c4518 fix getSubGroupClients for enable/disable and edit clients. 2025-01-31 17:27:09 +03:30
AghayeCoder
a787ab497c switcher for outbound traffic 2025-01-31 17:24:03 +03:30
mhsanaei
3be204f272 v2.5.0 2025-01-28 00:09:20 +01:00
mhsanaei
de13729a97 update dependencies 2025-01-28 00:09:12 +01:00
mhsanaei
468eb8b908 Update release.yml
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2025-01-27 01:13:28 +01:00
mhsanaei
e95a748e77 docker
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2025-01-27 01:13:12 +01:00
mhsanaei
34e2d961f5 quiet build
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2025-01-27 01:09:39 +01:00
mhsanaei
b4a1d81444 [subJson] better direct options
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2025-01-26 19:43:02 +01:00
mhsanaei
46ef506aa6 TLS - serverNameToVerify 2025-01-26 19:33:50 +01:00
mhsanaei
51220917c4 outbound Traffic - default false
if you need it you need to change it manually to true
2025-01-26 11:22:42 +01:00
mhsanaei
b34956647b minor change 2025-01-26 11:07:45 +01:00
mhsanaei
5c4e2dfd39 default setting - scStreamUpServerSecs 2025-01-26 11:07:35 +01:00
Ivan Zorin
dd4c2adb37 Fix non-MultiUser dbInbounds (#2649) 2025-01-26 11:03:06 +01:00
mhsanaei
2dec7f48f5 bug fix - get client ips 2025-01-24 17:16:16 +01:00
Ali Rahimi
04cf250a54 json post base path bug fixed (#2647)
* json post base path bug fixed

* added comment field to group client management
2025-01-23 21:33:47 +01:00
mhsanaei
ac9ab828b5 GO v1.23.5 + update dependencies 2025-01-21 23:04:03 +01:00
Zahar Izmailov
4dd40f6f19 Add checkpoint handling in CloseDB function (#2646)
* Add checkpoint handling in CloseDB function

---------

Co-authored-by: Zakhar Izmaylov <ptdev@kedruss.ru>
2025-01-21 22:55:21 +01:00
mhsanaei
7911eeb69f XHTTP - scStreamUpServerSecs 2025-01-21 22:43:13 +01:00
Ali Rahimi
6e9180a665 Group Management of Subscription Clients (#2644)
* add group user with the same subscription id to all inbounds

* code format compare

* add await for reset client traffic

* en language changed

* added client traffic syncer job

* handle exist email duplicate in sub group

* multi reset and delete request for clients group

* add client traffic syncer setting option

* vi translate file updated

* auto open qr-modal bug fixed
2025-01-21 03:01:54 +01:00
Zahar Izmailov
66fe84181b Enhance database initialization in db.go (#2645)
- Updated GORM configuration to skip default transactions and prepare statements.
- Modified the database connection string to include caching and journal mode settings.
- Executed several PRAGMA statements to optimize SQLite performance and enable foreign key support.

These changes improve database handling and performance in the application.

Co-authored-by: Zakhar Izmaylov <ptdev@kedruss.ru>
2025-01-21 02:59:30 +01:00
Tara Rostami
7b7eb98acb Minor Fixes (CSS) (#2641) 2025-01-15 12:50:50 +01:00
Alex Churin
7eb5afdd8d wireguard modal fix (#2640) 2025-01-14 16:08:19 +01:00
xtclovver
522ccda71c Update translate.ru_RU.toml (#2637)
Fixed ru translation
2025-01-13 18:23:19 +01:00
mortefy
f780efb430 Fixed ru translation (#2638) 2025-01-13 18:22:35 +01:00
mhsanaei
783f1a073e time Location - local 2025-01-11 15:13:49 +01:00
Dmitiry Vinogradov
a4c38ec8ae fail2ban service in docker container (#2632)
docker container

Co-authored-by: Dmitrij Vinogradov <dmitrij.vinogradov@gmail.com>
2025-01-11 13:41:48 +01:00
Igor Semenov
0c47771671 Add Russian domains (#2635)
Add Russian domains
2025-01-11 13:39:32 +01:00
Tara Rostami
67920a1962 Minor Fixes (UI) (#2636)
* Minor Fixes (UI)

* Update custom.min.css
2025-01-11 13:38:26 +01:00
mhsanaei
49d3957c07 bug fix 2025-01-05 21:04:18 +01:00
mhsanaei
ee946ceab2 iplimit: ipRegex improved
When the client has MUX enabled, a TCP or UDP prefix appears before the IP address. We initially weren’t aware of this behavior, but we have now resolved the issue.
2025-01-05 18:58:51 +01:00
LoST
b650064177 Firewall improvements (#2630)
* The menu has been completed

*Added firewall shutdown

*Improved port removal process (optional)
2025-01-05 16:01:56 +01:00
mhsanaei
9fb9d7201e Add custom v2ray rules for Russia
runetfreedom/russia-v2ray-rules-dat
2025-01-05 15:39:40 +01:00
mhsanaei
4a3b9b913d bug fix - reality settings 2025-01-01 22:49:29 +01:00
mhsanaei
da674d44cf httpupgrade: remove host from header 2025-01-01 20:18:28 +01:00
LoST
cc3252531b Added "comment" in all languages (#2631)
- Added a "comment" in all languages

---------

Co-authored-by: mhsanaei <ho3ein.sanaei@gmail.com>
2025-01-01 19:13:55 +01:00
mhsanaei
284731deeb xmux - hMaxReusableSecs 2025-01-01 18:48:47 +01:00
mhsanaei
9bc5c1d070 tcpNoDelay to penetrate 2025-01-01 18:42:50 +01:00
mhsanaei
26a7700557 Xray core buggy version removed
v1.8.24 and
major >=25.1.1
2025-01-01 18:36:46 +01:00
mhsanaei
b6a919218a Xray Core v25.1.1 2025-01-01 18:30:36 +01:00
mhsanaei
6cc07254e0 Fallback - Reality
this is not my problem they don't mention it on their document
2024-12-30 00:38:49 +01:00
mhsanaei
4ad5a5aba4 update dependencies 2024-12-28 15:15:01 +01:00
mhsanaei
7ab8164de4 Xray Core v24.12.28 2024-12-28 15:14:39 +01:00
99 changed files with 4964 additions and 3039 deletions

View File

@@ -7,7 +7,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
@@ -31,6 +31,8 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
install: true
- name: Login to Docker Hub
uses: docker/login-action@v3

View File

@@ -18,7 +18,7 @@ jobs:
- 386
- armv5
- s390x
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -72,7 +72,7 @@ jobs:
export GOARCH=s390x
export CC=s390x-linux-gnu-gcc
fi
go build -o xui-release -v main.go
go build -ldflags "-w -s" -o xui-release -v main.go
mkdir x-ui
cp xui-release x-ui/
@@ -83,43 +83,43 @@ jobs:
cd x-ui/bin
# Download dependencies
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v24.12.18/"
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.3.6/"
if [ "${{ matrix.platform }}" == "amd64" ]; then
wget ${Xray_URL}Xray-linux-64.zip
wget -q ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip
rm -f Xray-linux-64.zip
elif [ "${{ matrix.platform }}" == "arm64" ]; then
wget ${Xray_URL}Xray-linux-arm64-v8a.zip
wget -q ${Xray_URL}Xray-linux-arm64-v8a.zip
unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-arm64-v8a.zip
elif [ "${{ matrix.platform }}" == "armv7" ]; then
wget ${Xray_URL}Xray-linux-arm32-v7a.zip
wget -q ${Xray_URL}Xray-linux-arm32-v7a.zip
unzip Xray-linux-arm32-v7a.zip
rm -f Xray-linux-arm32-v7a.zip
elif [ "${{ matrix.platform }}" == "armv6" ]; then
wget ${Xray_URL}Xray-linux-arm32-v6.zip
wget -q ${Xray_URL}Xray-linux-arm32-v6.zip
unzip Xray-linux-arm32-v6.zip
rm -f Xray-linux-arm32-v6.zip
elif [ "${{ matrix.platform }}" == "386" ]; then
wget ${Xray_URL}Xray-linux-32.zip
wget -q ${Xray_URL}Xray-linux-32.zip
unzip Xray-linux-32.zip
rm -f Xray-linux-32.zip
elif [ "${{ matrix.platform }}" == "armv5" ]; then
wget ${Xray_URL}Xray-linux-arm32-v5.zip
wget -q ${Xray_URL}Xray-linux-arm32-v5.zip
unzip Xray-linux-arm32-v5.zip
rm -f Xray-linux-arm32-v5.zip
elif [ "${{ matrix.platform }}" == "s390x" ]; then
wget ${Xray_URL}Xray-linux-s390x.zip
wget -q ${Xray_URL}Xray-linux-s390x.zip
unzip Xray-linux-s390x.zip
rm -f Xray-linux-s390x.zip
fi
rm -f geoip.dat geosite.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 -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
wget -O geoip_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geosite.dat
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget -q -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -q -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
wget -q -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -q -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
mv xray xray-linux-${{ matrix.platform }}
cd ../..

View File

@@ -1,7 +1,7 @@
#!/bin/sh
# Start fail2ban
fail2ban-client -x start
[ $X_UI_ENABLE_FAIL2BAN == "true" ] && fail2ban-client -x start
# Run x-ui
exec /app/x-ui

View File

@@ -27,14 +27,14 @@ case $1 in
esac
mkdir -p build/bin
cd build/bin
wget "https://github.com/XTLS/Xray-core/releases/download/v24.12.18/Xray-linux-${ARCH}.zip"
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.3.6/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "xray-linux-${FNAME}"
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
wget -O geoip_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geosite.dat
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget -q -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -q -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
wget -q -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -q -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
cd ../../

View File

@@ -1,7 +1,7 @@
# ========================================================
# Stage: Builder
# ========================================================
FROM golang:1.23-alpine AS builder
FROM golang:1.24-alpine AS builder
WORKDIR /app
ARG TARGETARCH
@@ -15,7 +15,7 @@ COPY . .
ENV CGO_ENABLED=1
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
RUN go build -o build/x-ui main.go
RUN go build -ldflags "-w -s" -o build/x-ui main.go
RUN ./DockerInit.sh "$TARGETARCH"
# ========================================================
@@ -48,6 +48,7 @@ RUN chmod +x \
/app/x-ui \
/usr/bin/x-ui
ENV X_UI_ENABLE_FAIL2BAN="true"
VOLUME [ "/etc/x-ui" ]
CMD [ "./x-ui" ]
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]

View File

@@ -1,4 +1,4 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
[English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>
@@ -244,7 +244,7 @@ location /sub {
## SO Recomendados
- Ubuntu 20.04+
- Ubuntu 22.04+
- Debian 11+
- CentOS 8+
- OpenEuler 22.03+
@@ -258,6 +258,7 @@ location /sub {
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Virtuozzo Linux 8+
- Windows x64
## Arquitecturas y Dispositivos Compatibles
@@ -580,7 +581,7 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
## Reconocimientos
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Licencia: **GPL-3.0**): _Reglas de enrutamiento mejoradas de v2ray/xray y v2ray/xray-clients con dominios iraníes integrados y un enfoque en seguridad y bloqueo de anuncios._
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _Un dominio alojado en Vietnam y una lista de bloqueo con la máxima eficiencia para vietnamitas._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _Este repositorio contiene reglas de enrutamiento de V2Ray actualizadas automáticamente basadas en datos de dominios y direcciones bloqueados en Rusia._
## Estrellas a lo largo del tiempo

528
README.fa_IR.md Normal file
View File

@@ -0,0 +1,528 @@
[English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/3x-ui-dark.png">
<img alt="3x-ui" src="./media/3x-ui-light.png">
</picture>
</p>
**یک پنل وب پیشرفته • ساخته شده بر پایه Xray Core**
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](#)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
> **سلب مسئولیت:** این پروژه صرفاً برای اهداف آموزشی و تحقیقاتی است. استفاده از آن برای مقاصد غیرقانونی یا در محیط‌های عملیاتی ممنوع است.
**اگر این پروژه برای شما مفید بوده، می‌توانید با دادن یک**:star2: از آن حمایت کنید.
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
<img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## نصب و ارتقا
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
## نصب نسخه‌های قدیمی (توصیه نمی‌شود)
برای نصب نسخه خاصی از دستور زیر استفاده کنید. مثال برای نسخه `v1.7.9`:
```
VERSION=v1.7.9 && bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
```
## گواهی SSL
<details>
<summary>جزئیات گواهی SSL</summary>
### ACME
برای مدیریت گواهی‌های SSL با استفاده از ACME:
1. اطمینان حاصل کنید دامنه شما به درستی به سرور متصل است.
2. دستور `x-ui` را در ترمینال اجرا کرده و گزینه `مدیریت گواهی SSL` را انتخاب کنید.
3. گزینه‌های زیر نمایش داده می‌شوند:
- **دریافت SSL:** دریافت گواهی SSL
- **لغو:** لغو گواهی‌های موجود
- **تمدید اجباری:** تمدید اجباری گواهی‌ها
- **نمایش دامنه‌های موجود:** نمایش تمام دامنه‌های دارای گواهی
- **تنظیم مسیر گواهی برای پنل:** تنظیم مسیر گواهی برای دامنه شما
### Certbot
نصب و استفاده از Certbot:
```sh
apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
### Cloudflare
اسکریپت داخلی برای دریافت گواهی SSL از Cloudflare. نیازمند:
- ایمیل ثبت‌شده در Cloudflare
- کلید API جهانی Cloudflare
- دامنه باید از طریق Cloudflare به سرور متصل باشد
**دریافت کلید API جهانی Cloudflare:**
1. دستور `x-ui` را اجرا و گزینه `گواهی SSL کلادفلر` را انتخاب کنید.
2. به لینک [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens) مراجعه کنید.
3. روی "View Global API Key" کلیک کنید:
![](media/APIKey1.PNG)
4. پس از احراز هویت، کلید API نمایش داده می‌شود:
![](media/APIKey2.png)
در هنگام استفاده، نام دامنه، ایمیل و کلید API را وارد کنید:
![](media/DetailEnter.png)
</details>
## نصب دستی و ارتقا
<details>
<summary>جزئیات نصب دستی</summary>
#### استفاده
1. دریافت آخرین نسخه از سرور:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
s390x) echo 's390x' ;;
*) XUI_ARCH="amd64" ;;
esac
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
```
2. نصب یا ارتقا:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
s390x) echo 's390x' ;;
*) XUI_ARCH="amd64" ;;
esac
cd /root/
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
cp x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/
mv x-ui/ /usr/local/
systemctl daemon-reload
systemctl enable x-ui
systemctl restart x-ui
```
</details>
## نصب با Docker
<details>
<summary>جزئیات Docker</summary>
#### استفاده
1. **نصب Docker:**
```sh
bash <(curl -sSL https://get.docker.com)
```
2. **کلون پروژه:**
```sh
git clone https://github.com/MHSanaei/3x-ui.git
cd 3x-ui
```
3. **راه‌اندازی سرویس:**
```sh
docker compose up -d
```
یا
```sh
docker run -itd \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest
```
4. **به‌روزرسانی:**
```sh
cd 3x-ui
docker compose down
docker compose pull 3x-ui
docker compose up -d
```
5. **حذف:**
```sh
docker stop 3x-ui
docker rm 3x-ui
cd --
rm -r 3x-ui
```
</details>
## تنظیمات Nginx
<details>
<summary>پیکربندی Reverse Proxy</summary>
#### Nginx Reverse Proxy
```nginx
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
#### مسیر فرعی در Nginx
- اطمینان حاصل کنید "URI Path" در تنظیمات پنل یکسان باشد.
- `url` در تنظیمات پنل باید با `/` پایان یابد.
```nginx
location /sub {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
</details>
## سیستم‌عامل‌های توصیه شده
- Ubuntu 22.04+
- Debian 11+
- CentOS 8+
- OpenEuler 22.03+
- Fedora 36+
- Arch Linux
- Parch Linux
- Manjaro
- Armbian
- AlmaLinux 8.0+
- Rocky Linux 8+
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Virtuozzo Linux 8+
- Windows x64
## معماری‌ها و دستگاه‌های پشتیبانی شده
<details>
<summary>جزئیات معماری‌ها و دستگاه‌ها</summary>
- **amd64**: معماری استاندارد برای کامپیوترهای شخصی و سرورها
- **x86 / i386**: سیستم‌های دسکتاپ و لپ‌تاپ
- **armv8 / arm64 / aarch64**: دستگاه‌های موبایل و embedded مانند Raspberry Pi 4
- **armv7 / arm / arm32**: دستگاه‌های قدیمی مانند Orange Pi Zero
- **armv6 / arm / arm32**: دستگاه‌های بسیار قدیمی مانند Raspberry Pi 1
- **armv5 / arm / arm32**: سیستم‌های embedded قدیمی
- **s390x**: کامپیوترهای IBM mainframe
</details>
## زبان‌های پشتیبانی شده
- انگلیسی
- فارسی
- چینی سنتی
- چینی ساده‌شده
- ژاپنی
- روسی
- ویتنامی
- اسپانیایی
- اندونزیایی
- اوکراینی
- ترکی
- پرتغالی (برزیل)
## ویژگی‌ها
- مانیتورینگ وضعیت سیستم
- جستجو در بین inboundها و کلاینت‌ها
- تم تاریک/روشن
- پشتیبانی از چند کاربر و پروتکل
- پروتکل‌های VMESS، VLESS، Trojan، Shadowsocks، Dokodemo-door، Socks، HTTP، WireGuard
- پشتیبانی از XTLS شامل RPRX-Direct، Vision، REALITY
- آمار ترافیک، محدودیت ترافیک، محدودیت زمانی
- تنظیمات سفارشی Xray
- پشتیبانی از HTTPS برای پنل
- دریافت خودکار گواهی SSL
- مسیرهای API اصلاح شده
- پشتیبانی از تغییر تنظیمات از طریق پنل
- امکان export/import دیتابیس
## تنظیمات پیش‌فرض پنل
<details>
<summary>جزئیات تنظیمات پیش‌فرض</summary>
### نام کاربری، رمز عبور، پورت و مسیر وب
در صورت عدم تغییر، این موارد به صورت تصادفی ایجاد می‌شوند (به جز Docker).
**تنظیمات پیش‌فرض Docker:**
- **نام کاربری:** admin
- **رمز عبور:** admin
- **پورت:** 2053
### مدیریت دیتابیس:
امکان Backup و Restore دیتابیس از طریق پنل.
- **مسیر دیتابیس:**
- `/etc/x-ui/x-ui.db`
### مسیر پایه وب
1. **بازنشانی مسیر:**
- اجرای دستور `x-ui`
- انتخاب گزینه `Reset Web Base Path`
2. **ساخت یا تنظیم مسیر:**
- مسیر به صورت تصادفی ساخته شده یا قابل تنظیم است
3. **مشاهده تنظیمات فعلی:**
- استفاده از دستور `x-ui settings` یا `View Current Settings` در `x-ui`
**توصیه امنیتی:**
- استفاده از مسیرهای طولانی و تصادفی برای افزایش امنیت
**مثال:**
- `http://ip:port/*webbasepath*/panel`
- `http://domain:port/*webbasepath*/panel`
</details>
## پیکربندی WARP
<details>
<summary>جزئیات WARP</summary>
#### استفاده
**برای نسخه‌های `v2.1.0` و جدیدتر:**
WARP به صورت داخلی پشتیبانی می‌شود. تنها نیاز به فعال‌سازی در پنل است.
</details>
## محدودیت IP
<details>
<summary>جزئیات محدودیت IP</summary>
#### استفاده
**توجه:** محدودیت IP در صورت استفاده از IP Tunnel کار نمی‌کند.
- **تا نسخه `v1.6.1`:**
- محدودیت IP به صورت داخلی در پنل وجود دارد
**برای نسخه‌های `v1.7.0` و جدیدتر:**
برای فعال‌سازی نیاز به نصب `fail2ban` است:
1. اجرای دستور `x-ui` و انتخاب `مدیریت محدودیت IP`
2. گزینه‌های موجود:
- **تغییر مدت زمان Ban**
- **حذف تمام Banها**
- **مشاهده لاگ‌ها**
- **وضعیت Fail2ban**
- **راه‌اندازی مجدد Fail2ban**
- **حذف Fail2ban**
3. تنظیم مسیر `Access log` در پنل به `./access.log` و ذخیره و راه‌اندازی مجدد Xray
- **قبل از نسخه `v2.1.3`:**
- تنظیم دستی `access.log` در تنظیمات Xray:
```sh
"log": {
"access": "./access.log",
"dnsLog": false,
"loglevel": "warning"
},
```
- **از نسخه `v2.1.3`:**
- امکان تنظیم `access.log` از طریق پنل
</details>
## ربات تلگرام
<details>
<summary>جزئیات ربات تلگرام</summary>
#### استفاده
ربات تلگرام برای اطلاع‌رسانی ترافیک، ورود به پنل، Backup دیتابیس و ... استفاده می‌شود. نیازمند تنظیم:
- توکن تلگرام
- Chat ID ادمین‌ها
- زمان اطلاع‌رسانی (Cron syntax)
- اطلاع‌رسانی انقضا
- اطلاع‌رسانی ترافیک
- Backup دیتابیس
- اطلاع‌رسانی مصرف CPU
**سینتکس نمونه:**
- `30 \* \* \* \* \*` - اطلاع در ثانیه 30 هر دقیقه
- `@hourly` - هر ساعت
- `@daily` - هر روز
### ویژگی‌های ربات
- گزارش دوره‌ای
- اطلاع ورود به پنل
- اطلاع مصرف CPU
- اطلاع پیش‌از موعد انقضا و ترافیک
- گزارش ترافیک کلاینت‌ها
- منوی مبتنی بر دستور
- جستجوی کلاینت بر اساس ایمیل
- بررسی inboundها
- بررسی وضعیت سرور
- دریافت Backup
- چندزبانه
### راه‌اندازی ربات
- شروع [Botfather](https://t.me/BotFather) در تلگرام:
![Botfather](./media/botfather.png)
- ساخت ربات جدید با دستور /newbot:
![Create new bot](./media/newbot.png)
- شروع ربات ساخته شده:
![token](./media/token.png)
- تنظیمات پنل:
![Panel Config](./media/panel-bot-config.png)
وارد کردن توکن و Chat ID (دریافت از [این ربات](https://t.me/useridinfobot)):
![User ID](./media/user-id.png)
</details>
## مسیرهای API
<details>
<summary>جزئیات API</summary>
#### استفاده
- [مستندات API](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
- `/login` با `POST` داده کاربر: `{username: '', password: ''}`
| Method | مسیر | عملکرد |
| :----: | ---------------------------------- | ------------------------------------------- |
| `GET` | `"/list"` | دریافت تمام inboundها |
| `GET` | `"/get/:id"` | دریافت inbound بر اساس id |
| `POST` | `"/add"` | افزودن inbound |
| `POST` | `"/del/:id"` | حذف inbound |
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
</details>
## متغیرهای محیطی
<details>
<summary>جزئیات متغیرها</summary>
#### استفاده
| متغیر | نوع | پیش‌فرض |
| ------------- | :--------------------------------------------: | :------------ |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER| `string` | `"bin"` |
مثال:
```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
</details>
## پیش‌نمایش
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
<img alt="3x-ui" src="./media/01-overview-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
<img alt="3x-ui" src="./media/02-inbounds-light.png">
</picture>
## قدردانی ویژه از
- [alireza0](https://github.com/alireza0/)
## تشکر و قدردانی
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (مجوز: **GPL-3.0**)
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (مجوز: **GPL-3.0**)
## Stargazers over Time
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)

View File

@@ -1,4 +1,4 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
[English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>
@@ -249,7 +249,7 @@ location /sub {
## Recommended OS
- Ubuntu 20.04+
- Ubuntu 22.04+
- Debian 11+
- CentOS 8+
- OpenEuler 22.03+
@@ -263,6 +263,7 @@ location /sub {
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Virtuozzo Linux 8+
- Windows x64
## Supported Architectures and Devices
@@ -589,7 +590,7 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
## Acknowledgment
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _This repository contains automatically updated V2Ray routing rules based on data on blocked domains and addresses in Russia._
## Stargazers over Time

View File

@@ -1,4 +1,4 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
[English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>
@@ -248,7 +248,7 @@ location /sub {
## Рекомендуемые ОС
- Ubuntu 20.04+
- Ubuntu 22.04+
- Debian 11+
- CentOS 8+
- OpenEuler 22.03+
@@ -262,6 +262,7 @@ location /sub {
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Virtuozzo Linux 8+
- Windows x64
## Поддерживаемые архитектуры и устройства
@@ -587,7 +588,7 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
## Благодарности
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _Этот репозиторий содержит автоматически обновляемые правила маршрутизации V2Ray на основе данных о заблокированных доменах и адресах в России._
## Число звёзд со временем

View File

@@ -1,4 +1,4 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
[English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>
@@ -245,7 +245,7 @@ location /sub {
## 建议使用的操作系统
- Ubuntu 20.04+
- Ubuntu 22.04+
- Debian 11+
- CentOS 8+
- OpenEuler 22.03+
@@ -259,6 +259,7 @@ location /sub {
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Virtuozzo Linux 8+
- Windows x64
## 支持的架构和设备
@@ -580,7 +581,7 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
## 致谢
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _This repository contains automatically updated V2Ray routing rules based on data on blocked domains and addresses in Russia._
## Star趋势

View File

@@ -1 +1 @@
2.4.11
2.5.6

View File

@@ -26,7 +26,7 @@ const (
)
func initModels() error {
models := []interface{}{
models := []any{
&model.User{},
&model.Inbound{},
&model.OutboundTraffics{},

View File

@@ -11,6 +11,7 @@ services:
- $PWD/cert/:/root/cert/
environment:
XRAY_VMESS_AEAD_FORCED: "false"
X_UI_ENABLE_FAIL2BAN: "true"
tty: true
network_mode: host
restart: unless-stopped

89
go.mod
View File

@@ -1,46 +1,45 @@
module x-ui
go 1.23.4
go 1.24.1
require (
github.com/gin-contrib/gzip v1.0.1
github.com/gin-contrib/sessions v1.0.1
github.com/gin-contrib/gzip v1.2.2
github.com/gin-contrib/sessions v1.0.2
github.com/gin-gonic/gin v1.10.0
github.com/goccy/go-json v0.10.4
github.com/mymmrac/telego v0.31.4
github.com/nicksnyder/go-i18n/v2 v2.4.1
github.com/goccy/go-json v0.10.5
github.com/mymmrac/telego v0.32.0
github.com/nicksnyder/go-i18n/v2 v2.5.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.3
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.24.11
github.com/valyala/fasthttp v1.58.0
github.com/xtls/xray-core v1.8.25-0.20241218133935-cab2fdefd321
github.com/shirou/gopsutil/v4 v4.25.2
github.com/valyala/fasthttp v1.59.0
github.com/xtls/xray-core v1.250306.0
go.uber.org/atomic v1.11.0
golang.org/x/text v0.21.0
google.golang.org/grpc v1.69.2
golang.org/x/text v0.23.0
google.golang.org/grpc v1.71.0
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12
)
require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/bytedance/sonic v1.12.6 // indirect
github.com/bytedance/sonic/loader v0.2.1 // indirect
github.com/cloudflare/circl v1.5.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
github.com/ebitengine/purego v0.8.1 // indirect
github.com/fasthttp/router v1.5.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/bytedance/sonic v1.13.1 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/fasthttp/router v1.5.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.23.0 // indirect
github.com/go-playground/validator/v10 v10.25.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect
@@ -49,29 +48,29 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.22.0 // indirect
github.com/onsi/ginkgo/v2 v2.23.1 // indirect
github.com/pires/go-proxyproto v0.8.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.48.2 // indirect
github.com/quic-go/quic-go v0.50.0 // indirect
github.com/refraction-networking/utls v1.6.7 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/sagernet/sing v0.5.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagernet/sing v0.6.3 // indirect
github.com/sagernet/sing-shadowsocks v0.2.7 // indirect
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.9.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
@@ -83,20 +82,20 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/mock v0.5.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.12.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.28.0 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect
google.golang.org/protobuf v1.36.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0 // indirect
lukechampine.com/blake3 v1.4.0 // indirect
)

209
go.sum
View File

@@ -4,38 +4,37 @@ github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 h1:Wo41lDOevRJS
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk=
github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fasthttp/router v1.5.3 h1:BFWXqa3e4thRI3MgPKTNtz0Oiq6UYN2OsEtb+YQ5TMI=
github.com/fasthttp/router v1.5.3/go.mod h1:b864KkDIapOYh77AVG/SNkwfRZ6k6ecWvD+ZRXmP5pw=
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fasthttp/router v1.5.4 h1:oxdThbBwQgsDIYZ3wR1IavsNl6ZS9WdjKukeMikOnC8=
github.com/fasthttp/router v1.5.4/go.mod h1:3/hysWq6cky7dTfzaaEPZGdptwjwx0qzTgFCKEWRjgc=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
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/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4=
github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI=
github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM=
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/gzip v1.2.2 h1:iUU/EYCM8ENfkjmZaVrxbjF/ZC267Iqv5S0MMCMEliI=
github.com/gin-contrib/gzip v1.2.2/go.mod h1:C1a5cacjlDsS20cKnHlZRCPUu57D3qH6B2pV0rl+Y/s=
github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
@@ -51,25 +50,25 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
@@ -88,11 +87,11 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -100,27 +99,27 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v0.31.4 h1:NpiNl0P/8eydknka/k6XaaaWVj5BKMlM3Ibba63QTBU=
github.com/mymmrac/telego v0.31.4/go.mod h1:T12js1PgbYDYznvoN05MSMuPMfWTYo7D9LKl5cPFWiI=
github.com/nicksnyder/go-i18n/v2 v2.4.1 h1:zwzjtX4uYyiaU02K5Ia3zSkpJZrByARkRB4V3YPrr0g=
github.com/nicksnyder/go-i18n/v2 v2.4.1/go.mod h1:++Pl70FR6Cki7hdzZRnEEqdc2dJt+SAGotyFg/SvZMk=
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
github.com/mymmrac/telego v0.32.0 h1:4X8C1l3k+opkk86r95+eQE8DxiS2LYlR61L/G7yreDY=
github.com/mymmrac/telego v0.32.0/go.mod h1:qS6NaRhJgcuEEBEMVCV79S2xCAuHq9O+ixwfLuRW31M=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/onsi/ginkgo/v2 v2.23.1 h1:Ox0cOPv/t8RzKJUfDo9ZKtRvBOJY369sFJnl00CjqwY=
github.com/onsi/ginkgo/v2 v2.23.1/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
@@ -135,41 +134,43 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo=
github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagernet/sing v0.6.3 h1:J1spMc6LMlqUvRjWjvNMAcbvACDneqxB9zxfLuS0UTE=
github.com/sagernet/sing v0.6.3/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk=
github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
@@ -178,8 +179,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
@@ -189,64 +190,66 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 h1:g1Cj7d+my6k/HHxLAyxPwyX8i7FGRr6ulBDMkBzg2BM=
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463/go.mod h1:BjIOLmkEEtAgloAiVUcYj0Mt+YU00JARZw8AEU0IwAg=
github.com/xtls/xray-core v1.8.25-0.20241218133935-cab2fdefd321 h1:vk+n1RmfhFCj5xSi4I6C3USpcUQ48H3lt/QrtARVz1M=
github.com/xtls/xray-core v1.8.25-0.20241218133935-cab2fdefd321/go.mod h1:DCaUwrBk1RIC7hWg/wGoAynE69g3ptua1sEr8i0BWxA=
github.com/xtls/xray-core v1.250306.0 h1:XZyZvSgcpAoVEGnFnxNdoHbSF7Kp77A/0TPk4lhv6rM=
github.com/xtls/xray-core v1.250306.0/go.mod h1:clXnUOnX6CKWBGgJY4ePYhb/EtTdSrUC7vPfT6m5p4c=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -260,8 +263,8 @@ gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0 h1:P+U/06iIKPQ3DLcg+zBfSCia1luZ2msPZrJ8jYDFPs0=
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
lukechampine.com/blake3 v1.4.0 h1:xDbKOZCVbnZsfzM6mHSYcGRHZ3YrLDzqz8XnV4uaD5w=
lukechampine.com/blake3 v1.4.0/go.mod h1:MQJNQCTnR+kwOP/JEZSxj3MaQjp80FOFSNMMHXcSeX0=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@@ -63,8 +63,8 @@ elif [[ "${release}" == "centos" ]]; then
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 2004 ]]; then
echo -e "${red} Please use Ubuntu 20 or higher version!${plain}\n" && exit 1
if [[ ${os_version} -lt 2204 ]]; then
echo -e "${red} Please use Ubuntu 22 or higher version!${plain}\n" && exit 1
fi
elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then
@@ -90,10 +90,14 @@ elif [[ "${release}" == "ol" ]]; then
if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use Oracle Linux 8 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "virtuozzo" ]]; then
if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use Virtuozzo Linux 8 or higher ${plain}\n" && exit 1
fi
else
echo -e "${red}Your operating system is not supported by this script.${plain}\n"
echo "Please ensure you are using one of the following supported operating systems:"
echo "- Ubuntu 20.04+"
echo "- Ubuntu 22.04+"
echo "- Debian 11+"
echo "- CentOS 8+"
echo "- OpenEuler 22.03+"
@@ -107,6 +111,7 @@ else
echo "- Oracle Linux 8+"
echo "- OpenSUSE Tumbleweed"
echo "- Amazon Linux 2023"
echo "- Virtuozzo Linux 8+"
exit 1
fi
@@ -118,7 +123,7 @@ install_base() {
centos | almalinux | rocky | ol)
yum -y update && yum install -y -q wget curl tar tzdata
;;
fedora | amzn)
fedora | amzn | virtuozzo)
dnf -y update && dnf install -y -q wget curl tar tzdata
;;
arch | manjaro | parch)
@@ -209,7 +214,7 @@ install_x-ui() {
exit 1
fi
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz
wget -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz
if [[ $? -ne 0 ]]; then
echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}"
exit 1
@@ -226,7 +231,7 @@ install_x-ui() {
url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz"
echo -e "Beginning to install x-ui $1"
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
wget -N -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
if [[ $? -ne 0 ]]; then
echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}"
exit 1
@@ -251,7 +256,7 @@ install_x-ui() {
chmod +x x-ui bin/xray-linux-$(arch)
cp -f x-ui.service /etc/systemd/system/
wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
wget -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
chmod +x /usr/local/x-ui/x-ui.sh
chmod +x /usr/bin/x-ui
config_after_install

View File

@@ -47,52 +47,52 @@ func InitLogger(level logging.Level) {
logger = newLogger
}
func Debug(args ...interface{}) {
func Debug(args ...any) {
logger.Debug(args...)
addToBuffer("DEBUG", fmt.Sprint(args...))
}
func Debugf(format string, args ...interface{}) {
func Debugf(format string, args ...any) {
logger.Debugf(format, args...)
addToBuffer("DEBUG", fmt.Sprintf(format, args...))
}
func Info(args ...interface{}) {
func Info(args ...any) {
logger.Info(args...)
addToBuffer("INFO", fmt.Sprint(args...))
}
func Infof(format string, args ...interface{}) {
func Infof(format string, args ...any) {
logger.Infof(format, args...)
addToBuffer("INFO", fmt.Sprintf(format, args...))
}
func Notice(args ...interface{}) {
func Notice(args ...any) {
logger.Notice(args...)
addToBuffer("NOTICE", fmt.Sprint(args...))
}
func Noticef(format string, args ...interface{}) {
func Noticef(format string, args ...any) {
logger.Noticef(format, args...)
addToBuffer("NOTICE", fmt.Sprintf(format, args...))
}
func Warning(args ...interface{}) {
func Warning(args ...any) {
logger.Warning(args...)
addToBuffer("WARNING", fmt.Sprint(args...))
}
func Warningf(format string, args ...interface{}) {
func Warningf(format string, args ...any) {
logger.Warningf(format, args...)
addToBuffer("WARNING", fmt.Sprintf(format, args...))
}
func Error(args ...interface{}) {
func Error(args ...any) {
logger.Error(args...)
addToBuffer("ERROR", fmt.Sprint(args...))
}
func Errorf(format string, args ...interface{}) {
func Errorf(format string, args ...any) {
logger.Errorf(format, args...)
addToBuffer("ERROR", fmt.Sprintf(format, args...))
}

View File

@@ -107,11 +107,16 @@ func (s *Server) initRouter() (*gin.Engine, error) {
SubJsonRules = ""
}
SubTitle, err := s.settingService.GetSubTitle()
if err != nil {
SubTitle = ""
}
g := engine.Group("/")
s.sub = NewSUBController(
g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates,
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules)
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle)
return engine, nil
}

View File

@@ -9,6 +9,7 @@ import (
)
type SUBController struct {
subTitle string
subPath string
subJsonPath string
subEncrypt bool
@@ -30,9 +31,11 @@ func NewSUBController(
jsonNoise string,
jsonMux string,
jsonRules string,
subTitle string,
) *SUBController {
sub := NewSubService(showInfo, rModel)
a := &SUBController{
subTitle: subTitle,
subPath: subPath,
subJsonPath: jsonPath,
subEncrypt: encrypt,
@@ -82,7 +85,7 @@ func (a *SUBController) subs(c *gin.Context) {
// Add headers
c.Writer.Header().Set("Subscription-Userinfo", header)
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
c.Writer.Header().Set("Profile-Title", subId)
c.Writer.Header().Set("Profile-Title", a.subTitle)
if a.subEncrypt {
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
@@ -116,7 +119,7 @@ func (a *SUBController) subJsons(c *gin.Context) {
// Add headers
c.Writer.Header().Set("Subscription-Userinfo", header)
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
c.Writer.Header().Set("Profile-Title", subId)
c.Writer.Header().Set("Profile-Title", a.subTitle)
c.String(200, jsonSub)
}

View File

@@ -18,10 +18,10 @@ import (
var defaultJson string
type SubJsonService struct {
configJson map[string]interface{}
configJson map[string]any
defaultOutbounds []json_util.RawMessage
fragment string
noises string
noises string
mux string
inboundService service.InboundService
@@ -29,10 +29,10 @@ type SubJsonService struct {
}
func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService {
var configJson map[string]interface{}
var configJson map[string]any
var defaultOutbounds []json_util.RawMessage
json.Unmarshal([]byte(defaultJson), &configJson)
if outboundSlices, ok := configJson["outbounds"].([]interface{}); ok {
if outboundSlices, ok := configJson["outbounds"].([]any); ok {
for _, defaultOutbound := range outboundSlices {
jsonBytes, _ := json.Marshal(defaultOutbound)
defaultOutbounds = append(defaultOutbounds, jsonBytes)
@@ -40,9 +40,9 @@ func NewSubJsonService(fragment string, noises string, mux string, rules string,
}
if rules != "" {
var newRules []interface{}
routing, _ := configJson["routing"].(map[string]interface{})
defaultRules, _ := routing["rules"].([]interface{})
var newRules []any
routing, _ := configJson["routing"].(map[string]any)
defaultRules, _ := routing["rules"].([]any)
json.Unmarshal([]byte(rules), &newRules)
defaultRules = append(newRules, defaultRules...)
routing["rules"] = defaultRules
@@ -61,7 +61,7 @@ func NewSubJsonService(fragment string, noises string, mux string, rules string,
configJson: configJson,
defaultOutbounds: defaultOutbounds,
fragment: fragment,
noises: noises,
noises: noises,
mux: mux,
SubService: subService,
}
@@ -148,10 +148,10 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
var newJsonArray []json_util.RawMessage
stream := s.streamData(inbound.StreamSettings)
externalProxies, ok := stream["externalProxy"].([]interface{})
externalProxies, ok := stream["externalProxy"].([]any)
if !ok || len(externalProxies) == 0 {
externalProxies = []interface{}{
map[string]interface{}{
externalProxies = []any{
map[string]any{
"forceTls": "same",
"dest": host,
"port": float64(inbound.Port),
@@ -163,7 +163,7 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
delete(stream, "externalProxy")
for _, ep := range externalProxies {
extPrxy := ep.(map[string]interface{})
extPrxy := ep.(map[string]any)
inbound.Listen = extPrxy["dest"].(string)
inbound.Port = int(extPrxy["port"].(float64))
newStream := stream
@@ -171,7 +171,7 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
case "tls":
if newStream["security"] != "tls" {
newStream["security"] = "tls"
newStream["tslSettings"] = map[string]interface{}{}
newStream["tslSettings"] = map[string]any{}
}
case "none":
if newStream["security"] != "none" {
@@ -191,7 +191,7 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
}
newOutbounds = append(newOutbounds, s.defaultOutbounds...)
newConfigJson := make(map[string]interface{})
newConfigJson := make(map[string]any)
for key, value := range s.configJson {
newConfigJson[key] = value
}
@@ -205,19 +205,19 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
return newJsonArray
}
func (s *SubJsonService) streamData(stream string) map[string]interface{} {
var streamSettings map[string]interface{}
func (s *SubJsonService) streamData(stream string) map[string]any {
var streamSettings map[string]any
json.Unmarshal([]byte(stream), &streamSettings)
security, _ := streamSettings["security"].(string)
if security == "tls" {
streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]interface{}))
streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any))
} else if security == "reality" {
streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]interface{}))
streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any))
}
delete(streamSettings, "sockopt")
if s.fragment != "" {
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpMptcp": true, "tcpNoDelay": true}`)
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpMptcp": true, "penetrate": true}`)
}
// remove proxy protocol
@@ -233,17 +233,17 @@ func (s *SubJsonService) streamData(stream string) map[string]interface{} {
return streamSettings
}
func (s *SubJsonService) removeAcceptProxy(setting interface{}) map[string]interface{} {
netSettings, ok := setting.(map[string]interface{})
func (s *SubJsonService) removeAcceptProxy(setting any) map[string]any {
netSettings, ok := setting.(map[string]any)
if ok {
delete(netSettings, "acceptProxyProtocol")
}
return netSettings
}
func (s *SubJsonService) tlsData(tData map[string]interface{}) map[string]interface{} {
tlsData := make(map[string]interface{}, 1)
tlsClientSettings, _ := tData["settings"].(map[string]interface{})
func (s *SubJsonService) tlsData(tData map[string]any) map[string]any {
tlsData := make(map[string]any, 1)
tlsClientSettings, _ := tData["settings"].(map[string]any)
tlsData["serverName"] = tData["serverName"]
tlsData["alpn"] = tData["alpn"]
@@ -256,9 +256,9 @@ func (s *SubJsonService) tlsData(tData map[string]interface{}) map[string]interf
return tlsData
}
func (s *SubJsonService) realityData(rData map[string]interface{}) map[string]interface{} {
rltyData := make(map[string]interface{}, 1)
rltyClientSettings, _ := rData["settings"].(map[string]interface{})
func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
rltyData := make(map[string]any, 1)
rltyClientSettings, _ := rData["settings"].(map[string]any)
rltyData["show"] = false
rltyData["publicKey"] = rltyClientSettings["publicKey"]
@@ -266,13 +266,13 @@ func (s *SubJsonService) realityData(rData map[string]interface{}) map[string]in
// Set random data
rltyData["spiderX"] = "/" + random.Seq(15)
shortIds, ok := rData["shortIds"].([]interface{})
shortIds, ok := rData["shortIds"].([]any)
if ok && len(shortIds) > 0 {
rltyData["shortId"] = shortIds[random.Num(len(shortIds))].(string)
} else {
rltyData["shortId"] = ""
}
serverNames, ok := rData["serverNames"].([]interface{})
serverNames, ok := rData["serverNames"].([]any)
if ok && len(serverNames) > 0 {
rltyData["serverName"] = serverNames[random.Num(len(serverNames))].(string)
} else {
@@ -329,7 +329,7 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
}
if inbound.Protocol == model.Shadowsocks {
var inboundSettings map[string]interface{}
var inboundSettings map[string]any
json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
method, _ := inboundSettings["method"].(string)
serverData[0].Method = method
@@ -357,12 +357,12 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
}
type Outbound struct {
Protocol string `json:"protocol"`
Tag string `json:"tag"`
StreamSettings json_util.RawMessage `json:"streamSettings"`
Mux json_util.RawMessage `json:"mux,omitempty"`
ProxySettings map[string]interface{} `json:"proxySettings,omitempty"`
Settings OutboundSettings `json:"settings,omitempty"`
Protocol string `json:"protocol"`
Tag string `json:"tag"`
StreamSettings json_util.RawMessage `json:"streamSettings"`
Mux json_util.RawMessage `json:"mux,omitempty"`
ProxySettings map[string]any `json:"proxySettings,omitempty"`
Settings OutboundSettings `json:"settings,omitempty"`
}
type OutboundSettings struct {

View File

@@ -141,9 +141,9 @@ func (s *SubService) getFallbackMaster(dest string, streamSettings string) (stri
return "", 0, "", err
}
var stream map[string]interface{}
var stream map[string]any
json.Unmarshal([]byte(streamSettings), &stream)
var masterStream map[string]interface{}
var masterStream map[string]any
json.Unmarshal([]byte(inbound.StreamSettings), &masterStream)
stream["security"] = masterStream["security"]
stream["tlsSettings"] = masterStream["tlsSettings"]
@@ -171,66 +171,66 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
if inbound.Protocol != model.VMESS {
return ""
}
obj := map[string]interface{}{
obj := map[string]any{
"v": "2",
"add": s.address,
"port": inbound.Port,
"type": "none",
}
var stream map[string]interface{}
var stream map[string]any
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
network, _ := stream["network"].(string)
obj["net"] = network
switch network {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{})
tcp, _ := stream["tcpSettings"].(map[string]any)
header, _ := tcp["header"].(map[string]any)
typeStr, _ := header["type"].(string)
obj["type"] = typeStr
if typeStr == "http" {
request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{})
request := header["request"].(map[string]any)
requestPath, _ := request["path"].([]any)
obj["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{})
headers, _ := request["headers"].(map[string]any)
obj["host"] = searchHost(headers)
}
case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{})
kcp, _ := stream["kcpSettings"].(map[string]any)
header, _ := kcp["header"].(map[string]any)
obj["type"], _ = header["type"].(string)
obj["path"], _ = kcp["seed"].(string)
case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{})
ws, _ := stream["wsSettings"].(map[string]any)
obj["path"] = ws["path"].(string)
if host, ok := ws["host"].(string); ok && len(host) > 0 {
obj["host"] = host
} else {
headers, _ := ws["headers"].(map[string]interface{})
headers, _ := ws["headers"].(map[string]any)
obj["host"] = searchHost(headers)
}
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
grpc, _ := stream["grpcSettings"].(map[string]any)
obj["path"] = grpc["serviceName"].(string)
obj["authority"] = grpc["authority"].(string)
if grpc["multiMode"].(bool) {
obj["type"] = "multi"
}
case "httpupgrade":
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any)
obj["path"] = httpupgrade["path"].(string)
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
obj["host"] = host
} else {
headers, _ := httpupgrade["headers"].(map[string]interface{})
headers, _ := httpupgrade["headers"].(map[string]any)
obj["host"] = searchHost(headers)
}
case "xhttp":
xhttp, _ := stream["xhttpSettings"].(map[string]interface{})
xhttp, _ := stream["xhttpSettings"].(map[string]any)
obj["path"] = xhttp["path"].(string)
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
obj["host"] = host
} else {
headers, _ := xhttp["headers"].(map[string]interface{})
headers, _ := xhttp["headers"].(map[string]any)
obj["host"] = searchHost(headers)
}
obj["mode"] = xhttp["mode"].(string)
@@ -238,8 +238,8 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
security, _ := stream["security"].(string)
obj["tls"] = security
if security == "tls" {
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
alpns, _ := tlsSetting["alpn"].([]interface{})
tlsSetting, _ := stream["tlsSettings"].(map[string]any)
alpns, _ := tlsSetting["alpn"].([]any)
if len(alpns) > 0 {
var alpn []string
for _, a := range alpns {
@@ -273,14 +273,14 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
obj["id"] = clients[clientIndex].ID
obj["scy"] = clients[clientIndex].Security
externalProxies, _ := stream["externalProxy"].([]interface{})
externalProxies, _ := stream["externalProxy"].([]any)
if len(externalProxies) > 0 {
links := ""
for index, externalProxy := range externalProxies {
ep, _ := externalProxy.(map[string]interface{})
ep, _ := externalProxy.(map[string]any)
newSecurity, _ := ep["forceTls"].(string)
newObj := map[string]interface{}{}
newObj := map[string]any{}
for key, value := range obj {
if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp" || key == "allowInsecure")) {
newObj[key] = value
@@ -313,7 +313,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
if inbound.Protocol != model.VLESS {
return ""
}
var stream map[string]interface{}
var stream map[string]any
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.GetClients(inbound)
clientIndex := -1
@@ -331,54 +331,54 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
switch streamNetwork {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{})
tcp, _ := stream["tcpSettings"].(map[string]any)
header, _ := tcp["header"].(map[string]any)
typeStr, _ := header["type"].(string)
if typeStr == "http" {
request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{})
request := header["request"].(map[string]any)
requestPath, _ := request["path"].([]any)
params["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{})
headers, _ := request["headers"].(map[string]any)
params["host"] = searchHost(headers)
params["headerType"] = "http"
}
case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{})
kcp, _ := stream["kcpSettings"].(map[string]any)
header, _ := kcp["header"].(map[string]any)
params["headerType"] = header["type"].(string)
params["seed"] = kcp["seed"].(string)
case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{})
ws, _ := stream["wsSettings"].(map[string]any)
params["path"] = ws["path"].(string)
if host, ok := ws["host"].(string); ok && len(host) > 0 {
params["host"] = host
} else {
headers, _ := ws["headers"].(map[string]interface{})
headers, _ := ws["headers"].(map[string]any)
params["host"] = searchHost(headers)
}
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
grpc, _ := stream["grpcSettings"].(map[string]any)
params["serviceName"] = grpc["serviceName"].(string)
params["authority"], _ = grpc["authority"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
case "httpupgrade":
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any)
params["path"] = httpupgrade["path"].(string)
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
params["host"] = host
} else {
headers, _ := httpupgrade["headers"].(map[string]interface{})
headers, _ := httpupgrade["headers"].(map[string]any)
params["host"] = searchHost(headers)
}
case "xhttp":
xhttp, _ := stream["xhttpSettings"].(map[string]interface{})
xhttp, _ := stream["xhttpSettings"].(map[string]any)
params["path"] = xhttp["path"].(string)
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
params["host"] = host
} else {
headers, _ := xhttp["headers"].(map[string]interface{})
headers, _ := xhttp["headers"].(map[string]any)
params["host"] = searchHost(headers)
}
params["mode"] = xhttp["mode"].(string)
@@ -386,8 +386,8 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
security, _ := stream["security"].(string)
if security == "tls" {
params["security"] = "tls"
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
alpns, _ := tlsSetting["alpn"].([]interface{})
tlsSetting, _ := stream["tlsSettings"].(map[string]any)
alpns, _ := tlsSetting["alpn"].([]any)
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
@@ -418,18 +418,18 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
if security == "reality" {
params["security"] = "reality"
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
realitySetting, _ := stream["realitySettings"].(map[string]any)
realitySettings, _ := searchKey(realitySetting, "settings")
if realitySetting != nil {
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
sNames, _ := sniValue.([]interface{})
sNames, _ := sniValue.([]any)
params["sni"] = sNames[random.Num(len(sNames))].(string)
}
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
params["pbk"], _ = pbkValue.(string)
}
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
shortIds, _ := sidValue.([]interface{})
shortIds, _ := sidValue.([]any)
params["sid"] = shortIds[random.Num(len(shortIds))].(string)
}
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
@@ -449,12 +449,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["security"] = "none"
}
externalProxies, _ := stream["externalProxy"].([]interface{})
externalProxies, _ := stream["externalProxy"].([]any)
if len(externalProxies) > 0 {
links := ""
for index, externalProxy := range externalProxies {
ep, _ := externalProxy.(map[string]interface{})
ep, _ := externalProxy.(map[string]any)
newSecurity, _ := ep["forceTls"].(string)
dest, _ := ep["dest"].(string)
port := int(ep["port"].(float64))
@@ -507,7 +507,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
if inbound.Protocol != model.Trojan {
return ""
}
var stream map[string]interface{}
var stream map[string]any
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.GetClients(inbound)
clientIndex := -1
@@ -525,54 +525,54 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
switch streamNetwork {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{})
tcp, _ := stream["tcpSettings"].(map[string]any)
header, _ := tcp["header"].(map[string]any)
typeStr, _ := header["type"].(string)
if typeStr == "http" {
request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{})
request := header["request"].(map[string]any)
requestPath, _ := request["path"].([]any)
params["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{})
headers, _ := request["headers"].(map[string]any)
params["host"] = searchHost(headers)
params["headerType"] = "http"
}
case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{})
kcp, _ := stream["kcpSettings"].(map[string]any)
header, _ := kcp["header"].(map[string]any)
params["headerType"] = header["type"].(string)
params["seed"] = kcp["seed"].(string)
case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{})
ws, _ := stream["wsSettings"].(map[string]any)
params["path"] = ws["path"].(string)
if host, ok := ws["host"].(string); ok && len(host) > 0 {
params["host"] = host
} else {
headers, _ := ws["headers"].(map[string]interface{})
headers, _ := ws["headers"].(map[string]any)
params["host"] = searchHost(headers)
}
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
grpc, _ := stream["grpcSettings"].(map[string]any)
params["serviceName"] = grpc["serviceName"].(string)
params["authority"], _ = grpc["authority"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
case "httpupgrade":
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any)
params["path"] = httpupgrade["path"].(string)
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
params["host"] = host
} else {
headers, _ := httpupgrade["headers"].(map[string]interface{})
headers, _ := httpupgrade["headers"].(map[string]any)
params["host"] = searchHost(headers)
}
case "xhttp":
xhttp, _ := stream["xhttpSettings"].(map[string]interface{})
xhttp, _ := stream["xhttpSettings"].(map[string]any)
params["path"] = xhttp["path"].(string)
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
params["host"] = host
} else {
headers, _ := xhttp["headers"].(map[string]interface{})
headers, _ := xhttp["headers"].(map[string]any)
params["host"] = searchHost(headers)
}
params["mode"] = xhttp["mode"].(string)
@@ -580,8 +580,8 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
security, _ := stream["security"].(string)
if security == "tls" {
params["security"] = "tls"
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
alpns, _ := tlsSetting["alpn"].([]interface{})
tlsSetting, _ := stream["tlsSettings"].(map[string]any)
alpns, _ := tlsSetting["alpn"].([]any)
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
@@ -608,18 +608,18 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
if security == "reality" {
params["security"] = "reality"
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
realitySetting, _ := stream["realitySettings"].(map[string]any)
realitySettings, _ := searchKey(realitySetting, "settings")
if realitySetting != nil {
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
sNames, _ := sniValue.([]interface{})
sNames, _ := sniValue.([]any)
params["sni"] = sNames[random.Num(len(sNames))].(string)
}
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
params["pbk"], _ = pbkValue.(string)
}
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
shortIds, _ := sidValue.([]interface{})
shortIds, _ := sidValue.([]any)
params["sid"] = shortIds[random.Num(len(shortIds))].(string)
}
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
@@ -639,12 +639,12 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["security"] = "none"
}
externalProxies, _ := stream["externalProxy"].([]interface{})
externalProxies, _ := stream["externalProxy"].([]any)
if len(externalProxies) > 0 {
links := ""
for index, externalProxy := range externalProxies {
ep, _ := externalProxy.(map[string]interface{})
ep, _ := externalProxy.(map[string]any)
newSecurity, _ := ep["forceTls"].(string)
dest, _ := ep["dest"].(string)
port := int(ep["port"].(float64))
@@ -698,11 +698,11 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
if inbound.Protocol != model.Shadowsocks {
return ""
}
var stream map[string]interface{}
var stream map[string]any
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.GetClients(inbound)
var settings map[string]interface{}
var settings map[string]any
json.Unmarshal([]byte(inbound.Settings), &settings)
inboundPassword := settings["password"].(string)
method := settings["method"].(string)
@@ -719,54 +719,54 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
switch streamNetwork {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{})
tcp, _ := stream["tcpSettings"].(map[string]any)
header, _ := tcp["header"].(map[string]any)
typeStr, _ := header["type"].(string)
if typeStr == "http" {
request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{})
request := header["request"].(map[string]any)
requestPath, _ := request["path"].([]any)
params["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{})
headers, _ := request["headers"].(map[string]any)
params["host"] = searchHost(headers)
params["headerType"] = "http"
}
case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{})
kcp, _ := stream["kcpSettings"].(map[string]any)
header, _ := kcp["header"].(map[string]any)
params["headerType"] = header["type"].(string)
params["seed"] = kcp["seed"].(string)
case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{})
ws, _ := stream["wsSettings"].(map[string]any)
params["path"] = ws["path"].(string)
if host, ok := ws["host"].(string); ok && len(host) > 0 {
params["host"] = host
} else {
headers, _ := ws["headers"].(map[string]interface{})
headers, _ := ws["headers"].(map[string]any)
params["host"] = searchHost(headers)
}
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
grpc, _ := stream["grpcSettings"].(map[string]any)
params["serviceName"] = grpc["serviceName"].(string)
params["authority"], _ = grpc["authority"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
case "httpupgrade":
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any)
params["path"] = httpupgrade["path"].(string)
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
params["host"] = host
} else {
headers, _ := httpupgrade["headers"].(map[string]interface{})
headers, _ := httpupgrade["headers"].(map[string]any)
params["host"] = searchHost(headers)
}
case "xhttp":
xhttp, _ := stream["xhttpSettings"].(map[string]interface{})
xhttp, _ := stream["xhttpSettings"].(map[string]any)
params["path"] = xhttp["path"].(string)
if host, ok := xhttp["host"].(string); ok && len(host) > 0 {
params["host"] = host
} else {
headers, _ := xhttp["headers"].(map[string]interface{})
headers, _ := xhttp["headers"].(map[string]any)
params["host"] = searchHost(headers)
}
params["mode"] = xhttp["mode"].(string)
@@ -775,8 +775,8 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
security, _ := stream["security"].(string)
if security == "tls" {
params["security"] = "tls"
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
alpns, _ := tlsSetting["alpn"].([]interface{})
tlsSetting, _ := stream["tlsSettings"].(map[string]any)
alpns, _ := tlsSetting["alpn"].([]any)
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
@@ -806,12 +806,12 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
}
externalProxies, _ := stream["externalProxy"].([]interface{})
externalProxies, _ := stream["externalProxy"].([]any)
if len(externalProxies) > 0 {
links := ""
for index, externalProxy := range externalProxies {
ep, _ := externalProxy.(map[string]interface{})
ep, _ := externalProxy.(map[string]any)
newSecurity, _ := ep["forceTls"].(string)
dest, _ := ep["dest"].(string)
port := int(ep["port"].(float64))
@@ -944,9 +944,9 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin
return strings.Join(remark, separationChar)
}
func searchKey(data interface{}, key string) (interface{}, bool) {
func searchKey(data any, key string) (any, bool) {
switch val := data.(type) {
case map[string]interface{}:
case map[string]any:
for k, v := range val {
if k == key {
return v, true
@@ -955,7 +955,7 @@ func searchKey(data interface{}, key string) (interface{}, bool) {
return result, true
}
}
case []interface{}:
case []any:
for _, v := range val {
if result, ok := searchKey(v, key); ok {
return result, true
@@ -965,19 +965,19 @@ func searchKey(data interface{}, key string) (interface{}, bool) {
return nil, false
}
func searchHost(headers interface{}) string {
data, _ := headers.(map[string]interface{})
func searchHost(headers any) string {
data, _ := headers.(map[string]any)
for k, v := range data {
if strings.EqualFold(k, "host") {
switch v.(type) {
case []interface{}:
hosts, _ := v.([]interface{})
case []any:
hosts, _ := v.([]any)
if len(hosts) > 0 {
return hosts[0].(string)
} else {
return ""
}
case interface{}:
case any:
return v.(string)
}
}

View File

@@ -7,17 +7,17 @@ import (
"x-ui/logger"
)
func NewErrorf(format string, a ...interface{}) error {
func NewErrorf(format string, a ...any) error {
msg := fmt.Sprintf(format, a...)
return errors.New(msg)
}
func NewError(a ...interface{}) error {
func NewError(a ...any) error {
msg := fmt.Sprintln(a...)
return errors.New(msg)
}
func Recover(msg string) interface{} {
func Recover(msg string) any {
panicErr := recover()
if panicErr != nil {
if msg != "" {

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory(global):typeof define==="function"&&define.amd?define(factory):factory(global)})(typeof self!=="undefined"?self:typeof window!=="undefined"?window:typeof global!=="undefined"?global:this,function(global){"use strict";var _Base64=global.Base64;var version="2.5.0";var buffer;if(typeof module!=="undefined"&&module.exports){try{buffer=eval("require('buffer').Buffer")}catch(err){buffer=undefined}}var b64chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var b64tab=function(bin){var t={};for(var i=0,l=bin.length;i<l;i++)t[bin.charAt(i)]=i;return t}(b64chars);var fromCharCode=String.fromCharCode;var cb_utob=function(c){if(c.length<2){var cc=c.charCodeAt(0);return cc<128?c:cc<2048?fromCharCode(192|cc>>>6)+fromCharCode(128|cc&63):fromCharCode(224|cc>>>12&15)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}else{var cc=65536+(c.charCodeAt(0)-55296)*1024+(c.charCodeAt(1)-56320);return fromCharCode(240|cc>>>18&7)+fromCharCode(128|cc>>>12&63)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}};var re_utob=/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;var utob=function(u){return u.replace(re_utob,cb_utob)};var cb_encode=function(ccc){var padlen=[0,2,1][ccc.length%3],ord=ccc.charCodeAt(0)<<16|(ccc.length>1?ccc.charCodeAt(1):0)<<8|(ccc.length>2?ccc.charCodeAt(2):0),chars=[b64chars.charAt(ord>>>18),b64chars.charAt(ord>>>12&63),padlen>=2?"=":b64chars.charAt(ord>>>6&63),padlen>=1?"=":b64chars.charAt(ord&63)];return chars.join("")};var btoa=global.btoa?function(b){return global.btoa(b)}:function(b){return b.replace(/[\s\S]{1,3}/g,cb_encode)};var _encode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(u){return(u.constructor===buffer.constructor?u:buffer.from(u)).toString("base64")}:function(u){return(u.constructor===buffer.constructor?u:new buffer(u)).toString("base64")}:function(u){return btoa(utob(u))};var encode=function(u,urisafe){return!urisafe?_encode(String(u)):_encode(String(u)).replace(/[+\/]/g,function(m0){return m0=="+"?"-":"_"}).replace(/=/g,"")};var encodeURI=function(u){return encode(u,true)};var re_btou=new RegExp(["[À-ß][€-¿]","[à-ï][€-¿]{2}","[ð-÷][€-¿]{3}"].join("|"),"g");var cb_btou=function(cccc){switch(cccc.length){case 4:var cp=(7&cccc.charCodeAt(0))<<18|(63&cccc.charCodeAt(1))<<12|(63&cccc.charCodeAt(2))<<6|63&cccc.charCodeAt(3),offset=cp-65536;return fromCharCode((offset>>>10)+55296)+fromCharCode((offset&1023)+56320);case 3:return fromCharCode((15&cccc.charCodeAt(0))<<12|(63&cccc.charCodeAt(1))<<6|63&cccc.charCodeAt(2));default:return fromCharCode((31&cccc.charCodeAt(0))<<6|63&cccc.charCodeAt(1))}};var btou=function(b){return b.replace(re_btou,cb_btou)};var cb_decode=function(cccc){var len=cccc.length,padlen=len%4,n=(len>0?b64tab[cccc.charAt(0)]<<18:0)|(len>1?b64tab[cccc.charAt(1)]<<12:0)|(len>2?b64tab[cccc.charAt(2)]<<6:0)|(len>3?b64tab[cccc.charAt(3)]:0),chars=[fromCharCode(n>>>16),fromCharCode(n>>>8&255),fromCharCode(n&255)];chars.length-=[0,0,2,1][padlen];return chars.join("")};var _atob=global.atob?function(a){return global.atob(a)}:function(a){return a.replace(/\S{1,4}/g,cb_decode)};var atob=function(a){return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g,""))};var _decode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(a){return(a.constructor===buffer.constructor?a:buffer.from(a,"base64")).toString()}:function(a){return(a.constructor===buffer.constructor?a:new buffer(a,"base64")).toString()}:function(a){return btou(_atob(a))};var decode=function(a){return _decode(String(a).replace(/[-_]/g,function(m0){return m0=="-"?"+":"/"}).replace(/[^A-Za-z0-9\+\/]/g,""))};var noConflict=function(){var Base64=global.Base64;global.Base64=_Base64;return Base64};global.Base64={VERSION:version,atob:atob,btoa:btoa,fromBase64:decode,toBase64:encode,utob:utob,encode:encode,encodeURI:encodeURI,btou:btou,decode:decode,noConflict:noConflict,__buffer__:buffer};if(typeof Object.defineProperty==="function"){var noEnum=function(v){return{value:v,enumerable:false,writable:true,configurable:true}};global.Base64.extendString=function(){Object.defineProperty(String.prototype,"fromBase64",noEnum(function(){return decode(this)}));Object.defineProperty(String.prototype,"toBase64",noEnum(function(urisafe){return encode(this,urisafe)}));Object.defineProperty(String.prototype,"toBase64URI",noEnum(function(){return encode(this,true)}))}}if(global["Meteor"]){Base64=global.Base64}if(typeof module!=="undefined"&&module.exports){module.exports.Base64=global.Base64}else if(typeof define==="function"&&define.amd){define([],function(){return global.Base64})}return{Base64:global.Base64}});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,103 +0,0 @@
const supportLangs = [
{
name: "English",
value: "en-US",
icon: "🇺🇸",
},
{
name: "فارسی",
value: "fa-IR",
icon: "🇮🇷",
},
{
name: "简体中文",
value: "zh-CN",
icon: "🇨🇳",
},
{
name: "繁體中文",
value: "zh-TW",
icon: "🇹🇼",
},
{
name: "日本語",
value: "ja-JP",
icon: "🇯🇵",
},
{
name: "Русский",
value: "ru-RU",
icon: "🇷🇺",
},
{
name: "Tiếng Việt",
value: "vi-VN",
icon: "🇻🇳",
},
{
name: "Español",
value: "es-ES",
icon: "🇪🇸",
},
{
name: "Indonesian",
value: "id-ID",
icon: "🇮🇩",
},
{
name: "Український",
value: "uk-UA",
icon: "🇺🇦",
},
{
name: "Türkçe",
value: "tr-TR",
icon: "🇹🇷",
},
{
name: "Português",
value: "pt-BR",
icon: "🇧🇷",
},
];
function getLang() {
let lang = getCookie("lang");
if (!lang) {
if (window.navigator) {
lang = window.navigator.language || window.navigator.userLanguage;
if (isSupportLang(lang)) {
setCookie("lang", lang, 150);
} else {
setCookie("lang", "en-US", 150);
window.location.reload();
}
} else {
setCookie("lang", "en-US", 150);
window.location.reload();
}
}
return lang;
}
function setLang(lang) {
if (!isSupportLang(lang)) {
lang = "en-US";
}
setCookie("lang", lang, 150);
window.location.reload();
}
function isSupportLang(lang) {
for (l of supportLangs) {
if (l.value === lang) {
return true;
}
}
return false;
}

View File

@@ -25,11 +25,11 @@ class DBInbound {
}
get totalGB() {
return toFixed(this.total / ONE_GB, 2);
return NumberFormatter.toFixed(this.total / SizeFormatter.ONE_GB, 2);
}
set totalGB(gb) {
this.total = toFixed(gb * ONE_GB, 0);
this.total = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
}
get isVMess() {

View File

@@ -60,6 +60,7 @@ const UTLS_FINGERPRINT = {
UTLS_QQ: "qq",
UTLS_RANDOM: "random",
UTLS_RANDOMIZED: "randomized",
UTLS_RONDOMIZEDNOALPN: "randomizednoalpn",
UTLS_UNSAFE: "unsafe",
};
@@ -493,6 +494,7 @@ class xHTTPStreamSettings extends XrayCommonClass {
headers = [],
scMaxBufferedPosts = 30,
scMaxEachPostBytes = "1000000",
scStreamUpServerSecs = "20-80",
noSSEHeader = false,
xPaddingBytes = "100-1000",
mode = MODE_OPTION.AUTO,
@@ -503,6 +505,7 @@ class xHTTPStreamSettings extends XrayCommonClass {
this.headers = headers;
this.scMaxBufferedPosts = scMaxBufferedPosts;
this.scMaxEachPostBytes = scMaxEachPostBytes;
this.scStreamUpServerSecs = scStreamUpServerSecs;
this.noSSEHeader = noSSEHeader;
this.xPaddingBytes = xPaddingBytes;
this.mode = mode;
@@ -523,6 +526,7 @@ class xHTTPStreamSettings extends XrayCommonClass {
XrayCommonClass.toHeaders(json.headers),
json.scMaxBufferedPosts,
json.scMaxEachPostBytes,
json.scStreamUpServerSecs,
json.noSSEHeader,
json.xPaddingBytes,
json.mode,
@@ -536,6 +540,7 @@ class xHTTPStreamSettings extends XrayCommonClass {
headers: XrayCommonClass.toV2Headers(this.headers, false),
scMaxBufferedPosts: this.scMaxBufferedPosts,
scMaxEachPostBytes: this.scMaxEachPostBytes,
scStreamUpServerSecs: this.scStreamUpServerSecs,
noSSEHeader: this.noSSEHeader,
xPaddingBytes: this.xPaddingBytes,
mode: this.mode,
@@ -550,6 +555,7 @@ class TlsStreamSettings extends XrayCommonClass {
maxVersion = TLS_VERSION_OPTION.TLS13,
cipherSuites = '',
rejectUnknownSni = false,
verifyPeerCertInNames = ['dns.google', 'cloudflare-dns.com'],
disableSystemRoot = false,
enableSessionResumption = false,
certificates = [new TlsStreamSettings.Cert()],
@@ -562,6 +568,7 @@ class TlsStreamSettings extends XrayCommonClass {
this.maxVersion = maxVersion;
this.cipherSuites = cipherSuites;
this.rejectUnknownSni = rejectUnknownSni;
this.verifyPeerCertInNames = Array.isArray(verifyPeerCertInNames) ? verifyPeerCertInNames.join(",") : verifyPeerCertInNames;
this.disableSystemRoot = disableSystemRoot;
this.enableSessionResumption = enableSessionResumption;
this.certs = certificates;
@@ -593,6 +600,7 @@ class TlsStreamSettings extends XrayCommonClass {
json.maxVersion,
json.cipherSuites,
json.rejectUnknownSni,
json.verifyPeerCertInNames,
json.disableSystemRoot,
json.enableSessionResumption,
certs,
@@ -608,6 +616,7 @@ class TlsStreamSettings extends XrayCommonClass {
maxVersion: this.maxVersion,
cipherSuites: this.cipherSuites,
rejectUnknownSni: this.rejectUnknownSni,
verifyPeerCertInNames: this.verifyPeerCertInNames.split(","),
disableSystemRoot: this.disableSystemRoot,
enableSessionResumption: this.enableSessionResumption,
certificates: TlsStreamSettings.toJsonArray(this.certs),
@@ -758,7 +767,7 @@ class RealityStreamSettings extends XrayCommonClass {
json.maxClient,
json.maxTimediff,
json.shortIds,
json.settings,
settings,
);
}
@@ -816,7 +825,7 @@ class SockoptStreamSettings extends XrayCommonClass {
mark = 0,
tproxy = "off",
tcpMptcp = false,
tcpNoDelay = false,
penetrate = false,
domainStrategy = DOMAIN_STRATEGY_OPTION.USE_IP,
tcpMaxSeg = 1440,
dialerProxy = "",
@@ -834,7 +843,7 @@ class SockoptStreamSettings extends XrayCommonClass {
this.mark = mark;
this.tproxy = tproxy;
this.tcpMptcp = tcpMptcp;
this.tcpNoDelay = tcpNoDelay;
this.penetrate = penetrate;
this.domainStrategy = domainStrategy;
this.tcpMaxSeg = tcpMaxSeg;
this.dialerProxy = dialerProxy;
@@ -855,7 +864,7 @@ class SockoptStreamSettings extends XrayCommonClass {
json.mark,
json.tproxy,
json.tcpMptcp,
json.tcpNoDelay,
json.penetrate,
json.domainStrategy,
json.tcpMaxSeg,
json.dialerProxy,
@@ -876,7 +885,7 @@ class SockoptStreamSettings extends XrayCommonClass {
mark: this.mark,
tproxy: this.tproxy,
tcpMptcp: this.tcpMptcp,
tcpNoDelay: this.tcpNoDelay,
penetrate: this.penetrate,
domainStrategy: this.domainStrategy,
tcpMaxSeg: this.tcpMaxSeg,
dialerProxy: this.dialerProxy,
@@ -1041,7 +1050,7 @@ class Allocate extends XrayCommonClass {
class Inbound extends XrayCommonClass {
constructor(
port = RandomUtil.randomIntRange(10000, 60000),
port = RandomUtil.randomInteger(10000, 60000),
listen = '',
protocol = Protocols.VLESS,
settings = null,
@@ -1217,7 +1226,7 @@ class Inbound extends XrayCommonClass {
}
reset() {
this.port = RandomUtil.randomIntRange(10000, 60000);
this.port = RandomUtil.randomInteger(10000, 60000);
this.listen = '';
this.protocol = Protocols.VMESS;
this.settings = Inbound.Settings.getSettings(Protocols.VMESS);
@@ -1293,7 +1302,7 @@ class Inbound extends XrayCommonClass {
}
}
return 'vmess://' + base64(JSON.stringify(obj, null, 2));
return 'vmess://' + Base64.encode(JSON.stringify(obj, null, 2));
}
genVLESSLink(address = '', port = this.port, forceTls, remark = '', clientId, flow) {
@@ -1465,7 +1474,7 @@ class Inbound extends XrayCommonClass {
if (this.isSS2022) password.push(settings.password);
if (this.isSSMultiUser) password.push(clientPassword);
let link = `ss://${safeBase64(settings.method + ':' + password.join(':'))}@${address}:${port}`;
let link = `ss://${Base64.encode(`${settings.method}:${password.join(':')}`, true)}@${address}:${port}`;
const url = new URL(link);
for (const [key, value] of params) {
url.searchParams.set(key, value)
@@ -1828,11 +1837,11 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
}
}
get _totalGB() {
return toFixed(this.totalGB / ONE_GB, 2);
return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
}
set _totalGB(gb) {
this.totalGB = toFixed(gb * ONE_GB, 0);
this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
}
};
@@ -1938,11 +1947,11 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
}
}
get _totalGB() {
return toFixed(this.totalGB / ONE_GB, 2);
return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
}
set _totalGB(gb) {
this.totalGB = toFixed(gb * ONE_GB, 0);
this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
}
};
Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
@@ -2090,11 +2099,11 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
}
}
get _totalGB() {
return toFixed(this.totalGB / ONE_GB, 2);
return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
}
set _totalGB(gb) {
this.totalGB = toFixed(gb * ONE_GB, 0);
this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
}
};
@@ -2254,11 +2263,11 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
}
}
get _totalGB() {
return toFixed(this.totalGB / ONE_GB, 2);
return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
}
set _totalGB(gb) {
this.totalGB = toFixed(gb * ONE_GB, 0);
this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
}
};

View File

@@ -39,6 +39,7 @@ const UTLS_FINGERPRINT = {
UTLS_QQ: "qq",
UTLS_RANDOM: "random",
UTLS_RANDOMIZED: "randomized",
UTLS_RONDOMIZEDNOALPN: "randomizednoalpn",
UTLS_UNSAFE: "unsafe",
};
@@ -85,6 +86,16 @@ const MODE_OPTION = {
STREAM_ONE: "stream-one",
};
const Address_Port_Strategy = {
NONE: "none",
SrvPortOnly: "srvportonly",
SrvAddressOnly: "srvaddressonly",
SrvPortAndAddress: "srvportandaddress",
TxtPortOnly: "txtportonly",
TxtAddressOnly: "txtaddressonly",
TxtPortAndAddress: "txtportandaddress"
};
Object.freeze(Protocols);
Object.freeze(SSMethods);
Object.freeze(TLS_FLOW_CONTROL);
@@ -94,7 +105,7 @@ Object.freeze(OutboundDomainStrategies);
Object.freeze(WireguardDomainStrategy);
Object.freeze(USERS_SECURITY);
Object.freeze(MODE_OPTION);
Object.freeze(Address_Port_Strategy);
class CommonClass {
@@ -293,9 +304,9 @@ class xHTTPStreamSettings extends CommonClass {
xmux = {
maxConcurrency: "16-32",
maxConnections: 0,
cMaxReuseTimes: "64-128",
cMaxLifetimeMs: 0,
hMaxRequestTimes: "800-900",
cMaxReuseTimes: 0,
hMaxRequestTimes: "600-900",
hMaxReusableSecs: "1800-3000",
hKeepAlivePeriod: 0,
},
) {
@@ -330,8 +341,8 @@ class xHTTPStreamSettings extends CommonClass {
maxConcurrency: this.xmux.maxConcurrency,
maxConnections: this.xmux.maxConnections,
cMaxReuseTimes: this.xmux.cMaxReuseTimes,
cMaxLifetimeMs: this.xmux.cMaxLifetimeMs,
hMaxRequestTimes: this.xmux.hMaxRequestTimes,
hMaxReusableSecs: this.xmux.hMaxReusableSecs,
hKeepAlivePeriod: this.xmux.hKeepAlivePeriod,
},
};
@@ -411,14 +422,16 @@ class SockoptStreamSettings extends CommonClass {
tcpFastOpen = false,
tcpKeepAliveInterval = 0,
tcpMptcp = false,
tcpNoDelay = false
penetrate = false,
addressPortStrategy = Address_Port_Strategy.NONE,
) {
super();
this.dialerProxy = dialerProxy;
this.tcpFastOpen = tcpFastOpen;
this.tcpKeepAliveInterval = tcpKeepAliveInterval;
this.tcpMptcp = tcpMptcp;
this.tcpNoDelay = tcpNoDelay;
this.penetrate = penetrate;
this.addressPortStrategy = addressPortStrategy;
}
static fromJson(json = {}) {
@@ -428,7 +441,8 @@ class SockoptStreamSettings extends CommonClass {
json.tcpFastOpen,
json.tcpKeepAliveInterval,
json.tcpMptcp,
json.tcpNoDelay,
json.penetrate,
json.addressPortStrategy
);
}
@@ -438,7 +452,8 @@ class SockoptStreamSettings extends CommonClass {
tcpFastOpen: this.tcpFastOpen,
tcpKeepAliveInterval: this.tcpKeepAliveInterval,
tcpMptcp: this.tcpMptcp,
tcpNoDelay: this.tcpNoDelay,
penetrate: this.penetrate,
addressPortStrategy: this.addressPortStrategy
};
}
}

View File

@@ -26,11 +26,14 @@ class AllSetting {
this.xrayTemplateConfig = "";
this.secretEnable = false;
this.subEnable = false;
this.subTitle = "";
this.subListen = "";
this.subPort = 2096;
this.subPath = "/sub/";
this.subJsonPath = "/json/";
this.subDomain = "";
this.externalTrafficInformEnable = false;
this.externalTrafficInformURI = "";
this.subCertFile = "";
this.subKeyFile = "";
this.subUpdates = 12;
@@ -43,7 +46,7 @@ class AllSetting {
this.subJsonMux = "";
this.subJsonRules = "";
this.timeLocation = "Asia/Tehran";
this.timeLocation = "Local";
if (data == null) {
return

View File

@@ -1,194 +0,0 @@
const ONE_KB = 1024;
const ONE_MB = ONE_KB * 1024;
const ONE_GB = ONE_MB * 1024;
const ONE_TB = ONE_GB * 1024;
const ONE_PB = ONE_TB * 1024;
function sizeFormat(size) {
if (size <= 0) return "0 B";
if (size < ONE_KB) {
return size.toFixed(0) + " B";
} else if (size < ONE_MB) {
return (size / ONE_KB).toFixed(2) + " KB";
} else if (size < ONE_GB) {
return (size / ONE_MB).toFixed(2) + " MB";
} else if (size < ONE_TB) {
return (size / ONE_GB).toFixed(2) + " GB";
} else if (size < ONE_PB) {
return (size / ONE_TB).toFixed(2) + " TB";
} else {
return (size / ONE_PB).toFixed(2) + " PB";
}
}
function cpuSpeedFormat(speed) {
if (speed > 1000) {
const GHz = speed / 1000;
return GHz.toFixed(2) + " GHz";
} else {
return speed.toFixed(2) + " MHz";
}
}
function cpuCoreFormat(cores) {
if (cores === 1) {
return "1 Core";
} else {
return cores + " Cores";
}
}
function base64(str) {
return Base64.encode(str);
}
function safeBase64(str) {
return base64(str)
.replace(/\+/g, '-')
.replace(/=/g, '')
.replace(/\//g, '_');
}
function formatSecond(second) {
if (second < 60) {
return second.toFixed(0) + 's';
} else if (second < 3600) {
return (second / 60).toFixed(0) + 'm';
} else if (second < 3600 * 24) {
return (second / 3600).toFixed(0) + 'h';
} else {
day = Math.floor(second / 3600 / 24);
remain = ((second / 3600) - (day * 24)).toFixed(0);
return day + 'd' + (remain > 0 ? ' ' + remain + 'h' : '');
}
}
function addZero(num) {
if (num < 10) {
return "0" + num;
} else {
return num;
}
}
function toFixed(num, n) {
n = Math.pow(10, n);
return Math.floor(num * n) / n;
}
function debounce(fn, delay) {
var timeoutID = null;
return function () {
clearTimeout(timeoutID);
var args = arguments;
var that = this;
timeoutID = setTimeout(function () {
fn.apply(that, args);
}, delay);
};
}
function getCookie(cname) {
let name = cname + '=';
let ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
// decode cookie value only
return decodeURIComponent(c.substring(name.length, c.length));
}
}
return '';
}
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
let expires = 'expires=' + d.toUTCString();
// encode cookie value
document.cookie = cname + '=' + encodeURIComponent(cvalue) + ';' + expires + ';path=/';
}
function usageColor(data, threshold, total) {
switch (true) {
case data === null:
return "purple";
case total < 0:
return "green";
case total == 0:
return "purple";
case data < total - threshold:
return "green";
case data < total:
return "orange";
default:
return "red";
}
}
function clientUsageColor(clientStats, trafficDiff) {
switch (true) {
case !clientStats || clientStats.total == 0:
return "#7a316f"; // purple
case clientStats.up + clientStats.down < clientStats.total - trafficDiff:
return "#008771"; // Green
case clientStats.up + clientStats.down < clientStats.total:
return "#f37b24"; // Orange
default:
return "#cf3c3c"; // Red
}
}
function userExpiryColor(threshold, client, isDark = false) {
if (!client.enable) {
return isDark ? '#2c3950' : '#bcbcbc';
}
now = new Date().getTime(),
expiry = client.expiryTime;
switch (true) {
case expiry === null:
return "#7a316f"; // purple
case expiry < 0:
return "#008771"; // Green
case expiry == 0:
return "#7a316f"; // purple
case now < expiry - threshold:
return "#008771"; // Green
case now < expiry:
return "#f37b24"; // Orange
default:
return "#cf3c3c"; // Red
}
}
function doAllItemsExist(array1, array2) {
for (let i = 0; i < array1.length; i++) {
if (!array2.includes(array1[i])) {
return false;
}
}
return true;
}
function buildURL({ host, port, isTLS, base, path }) {
if (!host || host.length === 0) host = window.location.hostname;
if (!port || port.length === 0) port = window.location.port;
if (isTLS === undefined) isTLS = window.location.protocol === "https:";
const protocol = isTLS ? "https:" : "http:";
port = String(port);
if (port === "" || (isTLS && port === "443") || (!isTLS && port === "80")) {
port = "";
} else {
port = `:${port}`;
}
return `${protocol}//${host}${port}${base}${path}`;
}

View File

@@ -108,14 +108,14 @@ Date.prototype.setMaxTime = function () {
* Formatting date
*/
Date.prototype.formatDate = function () {
return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate());
return this.getFullYear() + "-" + NumberFormatter.addZero(this.getMonth() + 1) + "-" + NumberFormatter.addZero(this.getDate());
};
/**
* Format time
*/
Date.prototype.formatTime = function () {
return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds());
return NumberFormatter.addZero(this.getHours()) + ":" + NumberFormatter.addZero(this.getMinutes()) + ":" + NumberFormatter.addZero(this.getSeconds());
};
/**

View File

@@ -80,66 +80,68 @@ class PromiseUtil {
}
}
const seq = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
class RandomUtil {
static randomIntRange(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
static randomInt(n) {
return this.randomIntRange(0, n);
}
static randomSeq(count) {
let str = '';
for (let i = 0; i < count; ++i) {
str += seq[this.randomInt(62)];
static getSeq({ type = "default", hasNumbers = true, hasLowercase = true, hasUppercase = true } = {}) {
let seq = '';
switch (type) {
case "hex":
seq += "0123456789abcdef";
break;
default:
if (hasNumbers) seq += "0123456789";
if (hasLowercase) seq += "abcdefghijklmnopqrstuvwxyz";
if (hasUppercase) seq += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
break;
}
return str;
return seq;
}
static randomInteger(min, max) {
const range = max - min + 1;
const randomBuffer = new Uint32Array(1);
window.crypto.getRandomValues(randomBuffer);
return Math.floor((randomBuffer[0] / (0xFFFFFFFF + 1)) * range) + min;
}
static randomSeq(count, options = {}) {
const seq = this.getSeq(options);
const seqLength = seq.length;
const randomValues = new Uint32Array(count);
window.crypto.getRandomValues(randomValues);
return Array.from(randomValues, v => seq[v % seqLength]).join('');
}
static randomShortIds() {
const lengths = [2, 4, 6, 8, 10, 12, 14, 16];
for (let i = lengths.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[lengths[i], lengths[j]] = [lengths[j], lengths[i]];
}
const lengths = [2, 4, 6, 8, 10, 12, 14, 16].sort(() => Math.random() - 0.5);
let shortIds = [];
for (let length of lengths) {
let shortId = '';
for (let i = 0; i < length; i++) {
shortId += seq[this.randomInt(16)];
}
shortIds.push(shortId);
}
return shortIds.join(',');
return lengths.map(len => this.randomSeq(len, { type: "hex" })).join(',');
}
static randomLowerAndNum(len) {
let str = '';
for (let i = 0; i < len; ++i) {
str += seq[this.randomInt(36)];
}
return str;
return this.randomSeq(len, { hasUppercase: false });
}
static randomUUID() {
const template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
return template.replace(/[xy]/g, function (c) {
const randomValues = new Uint8Array(1);
crypto.getRandomValues(randomValues);
let randomValue = randomValues[0] % 16;
let calculatedValue = (c === 'x') ? randomValue : (randomValue & 0x3 | 0x8);
return calculatedValue.toString(16);
});
if (window.location.protocol === "https:") {
return window.crypto.randomUUID();
} else {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
.replace(/[xy]/g, function (c) {
const randomValues = new Uint8Array(1);
window.crypto.getRandomValues(randomValues);
let randomValue = randomValues[0] % 16;
let calculatedValue = (c === 'x') ? randomValue : (randomValue & 0x3 | 0x8);
return calculatedValue.toString(16);
});
}
}
static randomShadowsocksPassword() {
let array = new Uint8Array(32);
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
return btoa(String.fromCharCode.apply(null, array));
return Base64.encode(String.fromCharCode(...array));
}
}
@@ -478,4 +480,304 @@ class Wireguard {
privateKey: secretKey.length > 0 ? secretKey : this.keyToBase64(privateKey)
};
}
}
class ClipboardManager {
static copyText(content = "") {
// !! here old way of copying is used because not everyone can afford https connection
return new Promise((resolve) => {
try {
const textarea = window.document.createElement('textarea');
textarea.style.fontSize = '12pt';
textarea.style.border = '0';
textarea.style.padding = '0';
textarea.style.margin = '0';
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
textarea.style.top = `${window.pageYOffset || document.documentElement.scrollTop}px`;
textarea.setAttribute('readonly', '');
textarea.value = content;
window.document.body.appendChild(textarea);
textarea.select();
window.document.execCommand("copy");
window.document.body.removeChild(textarea);
resolve(true)
} catch {
resolve(false)
}
})
}
}
class Base64 {
static encode(content = "", safe = false) {
if (safe) {
return Base64.encode(content)
.replace(/\+/g, '-')
.replace(/=/g, '')
.replace(/\//g, '_')
}
return window.btoa(
String.fromCharCode(...new TextEncoder().encode(content))
)
}
static decode(content = "") {
return new TextDecoder()
.decode(
Uint8Array.from(window.atob(content), c => c.charCodeAt(0))
)
}
}
class SizeFormatter {
static ONE_KB = 1024;
static ONE_MB = this.ONE_KB * 1024;
static ONE_GB = this.ONE_MB * 1024;
static ONE_TB = this.ONE_GB * 1024;
static ONE_PB = this.ONE_TB * 1024;
static sizeFormat(size) {
if (size <= 0) return "0 B";
if (size < this.ONE_KB) return size.toFixed(0) + " B";
if (size < this.ONE_MB) return (size / this.ONE_KB).toFixed(2) + " KB";
if (size < this.ONE_GB) return (size / this.ONE_MB).toFixed(2) + " MB";
if (size < this.ONE_TB) return (size / this.ONE_GB).toFixed(2) + " GB";
if (size < this.ONE_PB) return (size / this.ONE_TB).toFixed(2) + " TB";
return (size / this.ONE_PB).toFixed(2) + " PB";
}
}
class CPUFormatter {
static cpuSpeedFormat(speed) {
return speed > 1000 ? (speed / 1000).toFixed(2) + " GHz" : speed.toFixed(2) + " MHz";
}
static cpuCoreFormat(cores) {
return cores === 1 ? "1 Core" : cores + " Cores";
}
}
class TimeFormatter {
static formatSecond(second) {
if (second < 60) return second.toFixed(0) + 's';
if (second < 3600) return (second / 60).toFixed(0) + 'm';
if (second < 3600 * 24) return (second / 3600).toFixed(0) + 'h';
let day = Math.floor(second / 3600 / 24);
let remain = ((second / 3600) - (day * 24)).toFixed(0);
return day + 'd' + (remain > 0 ? ' ' + remain + 'h' : '');
}
}
class NumberFormatter {
static addZero(num) {
return num < 10 ? "0" + num : num;
}
static toFixed(num, n) {
n = Math.pow(10, n);
return Math.floor(num * n) / n;
}
}
class Utils {
static debounce(fn, delay) {
let timeoutID = null;
return function () {
clearTimeout(timeoutID);
let args = arguments;
let that = this;
timeoutID = setTimeout(() => fn.apply(that, args), delay);
};
}
}
class CookieManager {
static getCookie(cname) {
let name = cname + '=';
let ca = document.cookie.split(';');
for (let c of ca) {
c = c.trim();
if (c.indexOf(name) === 0) {
return decodeURIComponent(c.substring(name.length, c.length));
}
}
return '';
}
static setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
let expires = 'expires=' + d.toUTCString();
document.cookie = cname + '=' + encodeURIComponent(cvalue) + ';' + expires + ';path=/';
}
}
class ColorUtils {
static usageColor(data, threshold, total) {
switch (true) {
case data === null: return "purple";
case total < 0: return "green";
case total == 0: return "purple";
case data < total - threshold: return "green";
case data < total: return "orange";
default: return "red";
}
}
static clientUsageColor(clientStats, trafficDiff) {
switch (true) {
case !clientStats || clientStats.total == 0: return "#7a316f";
case clientStats.up + clientStats.down < clientStats.total - trafficDiff: return "#008771";
case clientStats.up + clientStats.down < clientStats.total: return "#f37b24";
default: return "#cf3c3c";
}
}
static userExpiryColor(threshold, client, isDark = false) {
if (!client.enable) return isDark ? '#2c3950' : '#bcbcbc';
let now = new Date().getTime(), expiry = client.expiryTime;
switch (true) {
case expiry === null: return "#7a316f";
case expiry < 0: return "#008771";
case expiry == 0: return "#7a316f";
case now < expiry - threshold: return "#008771";
case now < expiry: return "#f37b24";
default: return "#cf3c3c";
}
}
}
class ArrayUtils {
static doAllItemsExist(array1, array2) {
return array1.every(item => array2.includes(item));
}
}
class URLBuilder {
static 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}`;
}
}
class LanguageManager {
static supportedLanguages = [
{
name: "English",
value: "en-US",
icon: "🇺🇸",
},
{
name: "فارسی",
value: "fa-IR",
icon: "🇮🇷",
},
{
name: "简体中文",
value: "zh-CN",
icon: "🇨🇳",
},
{
name: "繁體中文",
value: "zh-TW",
icon: "🇹🇼",
},
{
name: "日本語",
value: "ja-JP",
icon: "🇯🇵",
},
{
name: "Русский",
value: "ru-RU",
icon: "🇷🇺",
},
{
name: "Tiếng Việt",
value: "vi-VN",
icon: "🇻🇳",
},
{
name: "Español",
value: "es-ES",
icon: "🇪🇸",
},
{
name: "Indonesian",
value: "id-ID",
icon: "🇮🇩",
},
{
name: "Український",
value: "uk-UA",
icon: "🇺🇦",
},
{
name: "Türkçe",
value: "tr-TR",
icon: "🇹🇷",
},
{
name: "Português",
value: "pt-BR",
icon: "🇧🇷",
}
]
static getLanguage() {
let lang = CookieManager.getCookie("lang");
if (!lang) {
if (window.navigator) {
lang = window.navigator.language || window.navigator.userLanguage;
if (LanguageManager.isSupportLanguage(lang)) {
CookieManager.setCookie("lang", lang, 150);
} else {
CookieManager.setCookie("lang", "en-US", 150);
window.location.reload();
}
} else {
CookieManager.setCookie("lang", "en-US", 150);
window.location.reload();
}
}
return lang;
}
static setLanguage(language) {
if (!LanguageManager.isSupportLanguage(language)) {
language = "en-US";
}
CookieManager.setCookie("lang", language, 150);
window.location.reload();
}
static isSupportLanguage(language) {
const languageFilter = LanguageManager.supportedLanguages.filter((lang) => {
return lang.value === language
})
return languageFilter.length > 0;
}
}

View File

@@ -31,11 +31,11 @@ func jsonMsg(c *gin.Context, msg string, err error) {
jsonMsgObj(c, msg, nil, err)
}
func jsonObj(c *gin.Context, obj interface{}, err error) {
func jsonObj(c *gin.Context, obj any, err error) {
jsonMsgObj(c, "", obj, err)
}
func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
func jsonMsgObj(c *gin.Context, msg string, obj any, err error) {
m := entity.Msg{
Obj: obj,
}

View File

@@ -10,53 +10,56 @@ import (
)
type Msg struct {
Success bool `json:"success"`
Msg string `json:"msg"`
Obj interface{} `json:"obj"`
Success bool `json:"success"`
Msg string `json:"msg"`
Obj any `json:"obj"`
}
type AllSetting struct {
WebListen string `json:"webListen" form:"webListen"`
WebDomain string `json:"webDomain" form:"webDomain"`
WebPort int `json:"webPort" form:"webPort"`
WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
WebBasePath string `json:"webBasePath" form:"webBasePath"`
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
PageSize int `json:"pageSize" form:"pageSize"`
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
RemarkModel string `json:"remarkModel" form:"remarkModel"`
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"`
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"`
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
TgCpu int `json:"tgCpu" form:"tgCpu"`
TgLang string `json:"tgLang" form:"tgLang"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
SecretEnable bool `json:"secretEnable" form:"secretEnable"`
SubEnable bool `json:"subEnable" form:"subEnable"`
SubListen string `json:"subListen" form:"subListen"`
SubPort int `json:"subPort" form:"subPort"`
SubPath string `json:"subPath" form:"subPath"`
SubDomain string `json:"subDomain" form:"subDomain"`
SubCertFile string `json:"subCertFile" form:"subCertFile"`
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
SubUpdates int `json:"subUpdates" form:"subUpdates"`
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
SubURI string `json:"subURI" form:"subURI"`
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"`
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
Datepicker string `json:"datepicker" form:"datepicker"`
WebListen string `json:"webListen" form:"webListen"`
WebDomain string `json:"webDomain" form:"webDomain"`
WebPort int `json:"webPort" form:"webPort"`
WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
WebBasePath string `json:"webBasePath" form:"webBasePath"`
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
PageSize int `json:"pageSize" form:"pageSize"`
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
RemarkModel string `json:"remarkModel" form:"remarkModel"`
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"`
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"`
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
TgCpu int `json:"tgCpu" form:"tgCpu"`
TgLang string `json:"tgLang" form:"tgLang"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
SecretEnable bool `json:"secretEnable" form:"secretEnable"`
SubEnable bool `json:"subEnable" form:"subEnable"`
SubTitle string `json:"subTitle" form:"subTitle"`
SubListen string `json:"subListen" form:"subListen"`
SubPort int `json:"subPort" form:"subPort"`
SubPath string `json:"subPath" form:"subPath"`
SubDomain string `json:"subDomain" form:"subDomain"`
SubCertFile string `json:"subCertFile" form:"subCertFile"`
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
SubUpdates int `json:"subUpdates" form:"subUpdates"`
ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"`
ExternalTrafficInformURI string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"`
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
SubURI string `json:"subURI" form:"subURI"`
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"`
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
Datepicker string `json:"datepicker" form:"datepicker"`
}
func (s *AllSetting) CheckValid() error {

View File

@@ -5,10 +5,8 @@
<script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/qs/qs.min.js"></script>
<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/common.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/utils.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/langs.js"></script>
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
<script>
const basePath = '{{ .base_path }}';
axios.defaults.baseURL = basePath;

View File

@@ -10,7 +10,7 @@
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag>
<tr-qr-bg class="qr-bg-sub">
<tr-qr-bg-inner class="qr-bg-sub-inner">
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" class="qr-cv"></canvas>
<canvas @click="copy(genSubLink(qrModal.client.subId))" id="qrCode-sub" class="qr-cv"></canvas>
</tr-qr-bg-inner>
</tr-qr-bg>
</tr-qr-box>
@@ -18,7 +18,7 @@
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}} Json</span></a-tag>
<tr-qr-bg class="qr-bg-sub">
<tr-qr-bg-inner class="qr-bg-sub-inner">
<canvas @click="copyToClipboard('qrCode-subJson',genSubJsonLink(qrModal.client.subId))" id="qrCode-subJson" class="qr-cv"></canvas>
<canvas @click="copy(genSubJsonLink(qrModal.client.subId))" id="qrCode-subJson" class="qr-cv"></canvas>
</tr-qr-bg-inner>
</tr-qr-bg>
</tr-qr-box>
@@ -27,7 +27,7 @@
<tr-qr-box class="qr-box">
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
<tr-qr-bg class="qr-bg">
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
<canvas @click="copy(row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
</tr-qr-bg>
</tr-qr-box>
</template>
@@ -41,7 +41,6 @@
dbInbound: new DBInbound(),
client: null,
qrcodes: [],
clipboard: null,
visible: false,
subId: '',
show: function(title = '', dbInbound, client) {
@@ -79,14 +78,12 @@
qrModal: qrModal,
},
methods: {
copyToClipboard(elementId, content) {
this.qrModal.clipboard = new ClipboardJS('#' + elementId, {
text: () => content,
});
this.qrModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}')
this.qrModal.clipboard.destroy();
});
copy(content) {
ClipboardManager
.copyText(content)
.then(() => {
app.$message.success('{{ i18n "copied" }}')
})
},
setQrCode(elementId, content) {
new QRious({

View File

@@ -7,7 +7,7 @@
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"
:download="txtModal.fileName">[[ txtModal.fileName ]]
</a-button>
<a-button type="primary" id="copy-btn">{{ i18n "copy" }}</a-button>
<a-button type="primary" @click="txtModal.copy(txtModal.content)">{{ i18n "copy" }}</a-button>
</template>
<a-input style="overflow-y: auto;" type="textarea" v-model="txtModal.content"
:autosize="{ minRows: 10, maxRows: 20}"></a-input>
@@ -20,24 +20,20 @@
content: '',
fileName: '',
qrcode: null,
clipboard: null,
visible: false,
show: function (title = '', content = '', fileName = '') {
this.title = title;
this.content = content;
this.fileName = fileName;
this.visible = true;
textModalApp.$nextTick(() => {
if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#copy-btn', {
text: () => this.content,
});
this.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}')
this.close();
});
}
});
},
copy: function (content = '') {
ClipboardManager
.copyText(content)
.then(() => {
app.$message.success('{{ i18n "copied" }}')
this.close();
})
},
close: function () {
this.visible = false;

View File

@@ -422,16 +422,16 @@
</a-input>
</a-form-item>
<a-form-item>
<password-input autocomplete="password" name="password" icon="lock" v-model.trim="user.password"
<a-password-input autocomplete="password" name="password" icon="lock" v-model.trim="user.password"
placeholder='{{ i18n "password" }}'
@keydown.enter.native="login">
</password-input>
</a-password-input>
</a-form-item>
<a-form-item v-if="secretEnable">
<password-input autocomplete="secret" name="secret" icon="key" v-model.trim="user.loginSecret"
<a-password-input autocomplete="secret" name="secret" icon="key" v-model.trim="user.loginSecret"
placeholder='{{ i18n "secretToken" }}'
@keydown.enter.native="login">
</password-input>
</a-password-input>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
@@ -449,9 +449,9 @@
<a-row justify="center" class="centered">
<a-col :span="24">
<a-select ref="selectLang" v-model="lang"
@change="setLang(lang)" style="width: 200px;"
@change="LanguageManager.setLanguage(lang)" style="width: 200px;"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" label="English" v-for="l in supportLangs">
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
@@ -461,7 +461,7 @@
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<theme-switch-login></theme-switch-login>
<a-theme-switch-login></a-theme-switch-login>
</a-row>
</a-form-item>
</a-form>
@@ -473,8 +473,8 @@
</transition>
</a-layout>
{{template "js" .}}
{{template "component/themeSwitcher" .}}
{{template "component/password" .}}
{{template "component/aThemeSwitch" .}}
{{template "component/aPasswordInput" .}}
<script>
class User {
constructor() {
@@ -493,7 +493,7 @@
lang: ""
},
async created() {
this.lang = getLang();
this.lang = LanguageManager.getLanguage();
this.secretEnable = await this.getSecretStatus();
},
methods: {

View File

@@ -14,10 +14,10 @@
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.first" }}' v-if="clientsBulkModal.emailMethod>1">
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
<a-input-number v-model.number="clientsBulkModal.firstNum" :min="1"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.last" }}' v-if="clientsBulkModal.emailMethod>1">
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
<a-input-number v-model.number="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.prefix" }}' v-if="clientsBulkModal.emailMethod>0">
<a-input v-model.trim="clientsBulkModal.emailPrefix"></a-input>
@@ -26,7 +26,7 @@
<a-input v-model.trim="clientsBulkModal.emailPostfix"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.clientCount" }}' v-if="clientsBulkModal.emailMethod < 2">
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
<a-input-number v-model.number="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "security" }}' v-if="inbound.protocol === Protocols.VMESS">
<a-select v-model="clientsBulkModal.security" :dropdown-class-name="themeSwitcher.currentTheme">
@@ -61,7 +61,7 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number style="width: 50%" v-model="clientsBulkModal.tgId" min="0"></a-input-number>
<a-input-number style="width: 50%" v-model.number="clientsBulkModal.tgId" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="app.ipLimitEnable">
<template slot="label">
@@ -73,7 +73,7 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model="clientsBulkModal.limitIp" min="0"></a-input-number>
<a-input-number v-model.number="clientsBulkModal.limitIp" min="0"></a-input-number>
</a-form-item>
<a-form-item>
<template slot="label">
@@ -85,7 +85,7 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
<a-input-number v-model.number="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
@@ -106,9 +106,9 @@
<a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss" :dropdown-class-name="themeSwitcher.currentTheme"
v-model="clientsBulkModal.expiryTime"></a-date-picker>
<persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
<a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="clientsBulkModal.expiryTime" v-model="clientsBulkModal.expiryTime">
</persian-datepicker>
</a-persian-datepicker>
</a-form-item>
<a-form-item v-if="clientsBulkModal.expiryTime != 0">
<template slot="label">

View File

@@ -1,40 +1,30 @@
{{define "menuItems"}}
<a-menu-item key="{{ .base_path }}panel/">
<a-icon type="dashboard"></a-icon>
<span>
<b>{{ i18n "menu.dashboard"}}</b>
</span>
<span>{{ i18n "menu.dashboard"}}</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}panel/inbounds">
<a-icon type="user"></a-icon>
<span>
<b>{{ i18n "menu.inbounds"}}</b>
</span>
<span>{{ i18n "menu.inbounds"}}</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}panel/settings">
<a-icon type="setting"></a-icon>
<span>
<b>{{ i18n "menu.settings"}}</b>
</span>
<span>{{ i18n "menu.settings"}}</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}panel/xray">
<a-icon type="tool"></a-icon>
<span>
<b>{{ i18n "menu.xray"}}</b>
</span>
<span>{{ i18n "menu.xray"}}</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}logout">
<a-icon type="logout"></a-icon>
<span>
<b>{{ i18n "menu.logout"}}</b>
</span>
<span>{{ i18n "menu.logout"}}</span>
</a-menu-item>
{{end}}
{{define "commonSider"}}
<a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md">
<theme-switch></theme-switch>
<a-theme-switch></a-theme-switch>
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']" @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}}
</a-menu>
@@ -43,7 +33,7 @@
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
</div>
<theme-switch></theme-switch>
<a-theme-switch></a-theme-switch>
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']" @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}}
</a-menu>

View File

@@ -0,0 +1,42 @@
{{define "component/customStatistic"}}
<template>
<a-statistic :title="title" :value="value">
<template #prefix>
<slot name="prefix"></slot>
</template>
<template #suffix>
<slot name="suffix"></slot>
</template>
</a-statistic>
</template>
{{end}}
{{define "component/aCustomStatistic"}}
<style>
.dark .ant-statistic-content {
color: var(--dark-color-text-primary)
}
.dark .ant-statistic-title {
color: rgba(255, 255, 255, 0.55)
}
.ant-statistic-content {
font-size: 16px;
}
</style>
<script>
Vue.component('a-custom-statistic', {
props: {
'title': {
type: String,
required: false,
},
'value': {
type: String,
required: false
}
},
template: `{{template "component/customStatistic"}}`,
});
</script>
{{end}}

View File

@@ -0,0 +1,57 @@
{{define "component/passwordInput"}}
<template>
<a-input :value="value" :type="showPassword ? 'text' : 'password'" :placeholder="placeholder"
:autocomplete="autocomplete" :name="name" @input="$emit('input', $event.target.value)">
<template v-if="icon" #prefix>
<a-icon :type="icon" style="font-size: 16px;" />
</template>
<template #addonAfter>
<a-icon :type="showPassword ? 'eye-invisible' : 'eye'" @click="toggleShowPassword" style="font-size: 16px;" />
</template>
</a-input>
</template>
{{end}}
{{define "component/aPasswordInput"}}
<script>
Vue.component('a-password-input', {
props: {
'title': {
type: String,
required: false,
},
'value': {
type: String,
required: false,
},
'placeholder': {
type: String,
required: false,
},
'autocomplete': {
type: String,
required: false,
},
'name': {
type: String,
required: false,
},
'icon': {
type: undefined,
required: false
}
},
template: `{{template "component/passwordInput"}}`,
data() {
return {
showPassword: false,
};
},
methods: {
toggleShowPassword() {
this.showPassword = !this.showPassword;
},
},
});
</script>
{{end}}

View File

@@ -2,26 +2,38 @@
<template>
<div>
<a-input :value="value" type="text" v-model="date" data-jdp class="persian-datepicker"
@input="$emit('input', convertToGregorian($event.target.value)); jalaliDatepicker.hide();"
:placeholder="placeholder">
@input="$emit('input', convertToGregorian($event.target.value)); jalaliDatepicker.hide();"
:placeholder="placeholder">
<template #addonAfter>
<a-icon type="calendar" style="font-size: 14px; opacity: 0.5;"/>
<a-icon type="calendar" style="font-size: 14px; opacity: 0.5;" />
</template>
</a-input>
</div>
</template>
{{end}}
{{define "component/persianDatepicker"}}
<link rel="stylesheet" href="{{ .base_path }}assets/persian-datepicker/persian-datepicker.min.css?{{ .cur_ver }}"/>
{{define "component/aPersianDatepicker"}}
<link rel="stylesheet" href="{{ .base_path }}assets/persian-datepicker/persian-datepicker.min.css?{{ .cur_ver }}" />
<script src="{{ .base_path }}assets/moment/moment-jalali.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/persian-datepicker/persian-datepicker.min.js?{{ .cur_ver }}"></script>
<script>
const persianDatepicker = {};
Vue.component('persian-datepicker', {
props: ['placeholder', 'format', 'value'],
Vue.component('a-persian-datepicker', {
props: {
'format': {
type: undefined,
required: false,
},
'value': {
type: String,
required: false,
},
'placeholder': {
type: String,
required: false,
},
},
template: `{{template "component/persianDatepickerTemplate"}}`,
data() {
return {
@@ -57,4 +69,4 @@
}
});
</script>
{{end}}
{{end}}

View File

@@ -0,0 +1,49 @@
{{define "component/settingListItem"}}
<a-list-item :style="{ padding: padding }">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta>
<template #title>
<slot name="title"></slot>
</template>
<template #description>
<slot name="description"></slot>
</template>
</a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
<slot name="control"></slot>
</a-col>
</a-row>
</a-list-item>
{{end}}
{{define "component/aSettingListItem"}}
<script>
Vue.component('a-setting-list-item', {
props: {
'paddings': {
type: String,
required: false,
defaultValue: "default",
validator: function (value) {
return ['small', 'default'].includes(value)
}
}
},
template: `{{ template "component/settingListItem" }}`,
computed: {
padding() {
switch (this.paddings) {
case "small":
return "10px 20px !important"
break;
case "default":
return "20px !important"
break;
}
}
}
})
</script>
{{end}}

View File

@@ -1,13 +1,9 @@
{{define "component/sortableTableTrigger"}}
<a-icon type="drag"
class="sortable-icon"
style="cursor: move;"
@mouseup="mouseUpHandler"
@mousedown="mouseDownHandler"
<a-icon type="drag" class="sortable-icon" style="cursor: move;" @mouseup="mouseUpHandler" @mousedown="mouseDownHandler"
@click="clickHandler" />
{{end}}
{{define "component/sortableTable"}}
{{define "component/aTableSortable"}}
<script>
const DRAGGABLE_ROW_CLASS = 'draggable-row';
const findParentRowElement = (el) => {
@@ -28,7 +24,16 @@
newElementIndex: null,
};
},
props: ['data-source', 'customRow'],
props: {
'data-source': {
type: undefined,
required: false,
},
'customRow': {
type: undefined,
required: false,
}
},
inheritAttrs: false,
provide() {
const sortable = {}
@@ -44,7 +49,7 @@
sortable,
}
},
render: function(createElement) {
render: function (createElement) {
return createElement('a-table', {
class: {
'ant-table-is-sorting': this.isDragging(),
@@ -59,7 +64,7 @@
drop: (e) => this.dropHandler(e),
},
scopedSlots: this.$scopedSlots,
}, this.$slots.default, )
}, this.$slots.default,)
},
created() {
this.$memoSort = {};
@@ -163,9 +168,14 @@
}
}
});
Vue.component('table-sort-trigger', {
Vue.component('a-table-sort-trigger', {
template: `{{template "component/sortableTableTrigger"}}`,
props: ['item-index'],
props: {
'item-index': {
type: undefined,
required: false
}
},
inject: ['sortable'],
methods: {
mouseDownHandler(e) {
@@ -190,27 +200,33 @@
display: none;
}
}
.ant-table-is-sorting .draggable-row td {
background-color: #ffffff !important;
}
.dark .ant-table-is-sorting .draggable-row td {
background-color: var(--dark-color-surface-100) !important;
}
.ant-table-is-sorting .dragging td {
background-color: rgb(232 244 242) !important;
color: rgba(0, 0, 0, 0.3);
}
.dark .ant-table-is-sorting .dragging td {
background-color: var(--dark-color-table-hover) !important;
color: rgba(255, 255, 255, 0.3);
}
.ant-table-is-sorting .dragging {
opacity: 1;
box-shadow: 1px -2px 2px #008771;
transition: all 0.2s;
}
.ant-table-is-sorting .dragging .ant-table-row-index {
opacity: 0.3;
}
</style>
{{end}}
{{end}}

View File

@@ -4,11 +4,15 @@
<a-sub-menu>
<span slot="title">
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
<span>Theme</span>
<span>{{ i18n "menu.theme" }}</span>
</span>
<a-menu-item id="change-theme" class="ant-menu-theme-switch" @mousedown="themeSwitcher.animationsOff()"> Dark <a-switch style="margin-left: 2px;" size="small" :default-checked="themeSwitcher.isDarkTheme" @change="themeSwitcher.toggleTheme()"></a-switch>
<a-menu-item id="change-theme" class="ant-menu-theme-switch" @mousedown="themeSwitcher.animationsOff()">
<span>{{ i18n "menu.dark" }}</span>
<a-switch style="margin-left: 2px;" size="small" :default-checked="themeSwitcher.isDarkTheme" @change="themeSwitcher.toggleTheme()"></a-switch>
</a-menu-item>
<a-menu-item id="change-theme-ultra" v-if="themeSwitcher.isDarkTheme" class="ant-menu-theme-switch" @mousedown="themeSwitcher.animationsOffUltra()"> Ultra <a-checkbox style="margin-left: 2px;" :checked="themeSwitcher.isUltra" @click="themeSwitcher.toggleUltra()"></a-checkbox>
<a-menu-item id="change-theme-ultra" v-if="themeSwitcher.isDarkTheme" class="ant-menu-theme-switch" @mousedown="themeSwitcher.animationsOffUltra()">
<span>{{ i18n "menu.ultraDark" }}</span>
<a-checkbox style="margin-left: 2px;" :checked="themeSwitcher.isUltra" @click="themeSwitcher.toggleUltra()"></a-checkbox>
</a-menu-item>
</a-sub-menu>
</a-menu>
@@ -17,19 +21,22 @@
{{define "component/themeSwitchTemplateLogin"}}
<template>
<a-menu @mousedown="themeSwitcher.animationsOff()" id="change-theme" :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
<a-menu @mousedown="themeSwitcher.animationsOff()" id="change-theme" :theme="themeSwitcher.currentTheme" mode="inline"
selected-keys="">
<a-menu-item mode="inline" class="ant-menu-theme-switch">
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
<a-switch size="small" :default-checked="themeSwitcher.isDarkTheme" @change="themeSwitcher.toggleTheme()"></a-switch>
<a-switch size="small" :default-checked="themeSwitcher.isDarkTheme"
@change="themeSwitcher.toggleTheme()"></a-switch>
<template v-if="themeSwitcher.isDarkTheme">
<a-checkbox style="margin-left: 1rem; vertical-align: middle;" :checked="themeSwitcher.isUltra" @click="themeSwitcher.toggleUltra()">Ultra</a-checkbox>
<a-checkbox style="margin-left: 1rem; vertical-align: middle;" :checked="themeSwitcher.isUltra"
@click="themeSwitcher.toggleUltra()">Ultra</a-checkbox>
</template>
</a-menu-item>
</a-menu>
</template>
{{end}}
{{define "component/themeSwitcher"}}
{{define "component/aThemeSwitch"}}
<script>
function createThemeSwitcher() {
const isDarkTheme = localStorage.getItem('dark-mode') === 'true';
@@ -83,8 +90,7 @@
};
}
const themeSwitcher = createThemeSwitcher();
Vue.component('theme-switch', {
props: [],
Vue.component('a-theme-switch', {
template: `{{template "component/themeSwitchTemplate"}}`,
data: () => ({
themeSwitcher
@@ -96,8 +102,7 @@
document.getElementById('message').className = themeSwitcher.currentTheme;
}
});
Vue.component('theme-switch-login', {
props: [],
Vue.component('a-theme-switch-login', {
template: `{{template "component/themeSwitchTemplateLogin"}}`,
data: () => ({
themeSwitcher
@@ -110,4 +115,4 @@
}
});
</script>
{{end}}
{{end}}

View File

@@ -1,37 +0,0 @@
{{define "component/passwordInput"}}
<template>
<a-input :value="value" :type="showPassword ? 'text' : 'password'"
:placeholder="placeholder"
:autocomplete="autocomplete"
:name="name"
@input="$emit('input', $event.target.value)">
<template v-if="icon" #prefix>
<a-icon :type="icon" style="font-size: 16px;" />
</template>
<template #addonAfter>
<a-icon :type="showPassword ? 'eye-invisible' : 'eye'"
@click="toggleShowPassword"
style="font-size: 16px;" />
</template>
</a-input>
</template>
{{end}}
{{define "component/password"}}
<script>
Vue.component('password-input', {
props: ["title", "value", "placeholder", "icon", "autocomplete", "name"],
template: `{{template "component/passwordInput"}}`,
data() {
return {
showPassword: false,
};
},
methods: {
toggleShowPassword() {
this.showPassword = !this.showPassword;
},
},
});
</script>
{{end}}

View File

@@ -1,36 +0,0 @@
{{define "component/settingListItem"}}
<a-list-item style="padding: 20px">
<a-row v-if="type === 'textarea'">
<a-col>
<a-list-item-meta :title="title" :description="desc"/>
<a-textarea class="ant-setting-textarea" :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10 }"></a-textarea>
<!--a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 30 }"></a-textarea-->
</a-col>
</a-row>
<a-row v-else>
<a-col :lg="24" :xl="12">
<a-list-item-meta :title="title" :description="desc"/>
</a-col>
<a-col :lg="24" :xl="12">
<template v-if="type === 'text'">
<a-input :value="value" @input="$emit('input', $event.target.value)" :placeholder="placeholder"></a-input>
</template>
<template v-else-if="type === 'number'">
<a-input-number :value="value" :step="step" @change="value => $emit('input', value)" :min="min" :max="max" style="width: 100%;"></a-input-number>
</template>
<template v-else-if="type === 'switch'">
<a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>
</template>
</a-col>
</a-row>
</a-list-item>
{{end}}
{{define "component/setting"}}
<script>
Vue.component('setting-list-item', {
props: ["type", "title", "desc", "value", "min", "max" , "step", "placeholder"],
template: `{{template "component/settingListItem"}}`,
});
</script>
{{end}}

View File

@@ -7,7 +7,7 @@
<a-input v-model.trim="fakednsModal.fakeDns.ipPool"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.fakedns.poolSize" }}'>
<a-input-number style="width: 100%;" type="number" min="1" v-model.trim="fakednsModal.fakeDns.poolSize"></a-input-number>
<a-input-number v-model.number="fakednsModal.fakeDns.poolSize" :min="1"></a-input-number>
</a-form-item>
</a-form>
</a-modal>

View File

@@ -66,9 +66,9 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number style="width: 50%" v-model="client.tgId" min="0"></a-input-number>
<a-input-number style="width: 50%" v-model.number="client.tgId" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="client.email" label='Comment'>
<a-form-item v-if="client.email" label='{{ i18n "comment" }}'>
<a-input v-model.trim="client.comment"></a-input>
</a-form-item>
<a-form-item v-if="app.ipLimitEnable">
@@ -81,7 +81,7 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
<a-input-number v-model.number="client.limitIp" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit">
<template slot="label">
@@ -123,13 +123,13 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
<a-input-number v-model.number="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="isEdit && clientStats" label='{{ i18n "usage" }}'>
<a-tag :color="clientUsageColor(clientStats, app.trafficDiff)">
[[ sizeFormat(clientStats.up) ]] /
[[ sizeFormat(clientStats.down) ]]
([[ sizeFormat(clientStats.up + clientStats.down) ]])
<a-tag :color="ColorUtils.clientUsageColor(clientStats, app.trafficDiff)">
[[ SizeFormatter.sizeFormat(clientStats.up) ]] /
[[ SizeFormatter.sizeFormat(clientStats.down) ]]
([[ SizeFormatter.sizeFormat(clientStats.up + clientStats.down) ]])
</a-tag>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
@@ -154,8 +154,8 @@
</template>
<a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.currentTheme" v-model="client._expiryTime"></a-date-picker>
<persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="client._expiryTime" v-model="client._expiryTime"></persian-datepicker>
<a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="client._expiryTime" v-model="client._expiryTime"></a-persian-datepicker>
<a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
</a-form-item>
<a-form-item v-if="client.expiryTime != 0">

View File

@@ -41,7 +41,7 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
<a-input-number v-model.number="dbInbound.totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
@@ -57,9 +57,9 @@
<a-date-picker style="width: 100%;" v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss" :dropdown-class-name="themeSwitcher.currentTheme"
v-model="dbInbound._expiryTime"></a-date-picker>
<persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
<a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="dbInbound._expiryTime" v-model="dbInbound._expiryTime">
</persian-datepicker>
</a-persian-datepicker>
</a-form-item>
</a-form>

View File

@@ -66,7 +66,7 @@
</a-divider>
<a-form-item label='Type'>
<a-select v-model="noise.type" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['rand','base64','str']" :value="s">[[ s ]]</a-select-option>
<a-select-option v-for="s in ['rand','base64','str', 'hex']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Packet'>
@@ -392,14 +392,14 @@
<a-form-item label="Max Reuse Times">
<a-input v-model="outbound.stream.xhttp.xmux.cMaxReuseTimes"></a-input>
</a-form-item>
<a-form-item label="Max Lifetime (ms)">
<a-input v-model="outbound.stream.xhttp.xmux.cMaxLifetimeMs"></a-input>
</a-form-item>
<a-form-item label="Max Request Times">
<a-input v-model="outbound.stream.xhttp.xmux.hMaxRequestTimes"></a-input>
</a-form-item>
<a-form-item label="Max Reusable Secs">
<a-input v-model="outbound.stream.xhttp.xmux.hMaxReusableSecs"></a-input>
</a-form-item>
<a-form-item label='Keep Alive Period'>
<a-input v-model.number="outbound.stream.xhttp.xmux.hKeepAlivePeriod"></a-input>
<a-input-number v-model.number="outbound.stream.xhttp.xmux.hKeepAlivePeriod"></a-input-number>
</a-form-item>
</template>
</template>
@@ -465,17 +465,22 @@
<a-select-option v-for="tag in ['', ...outModal.tags]" :value="tag">[[ tag ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="TCP Fast Open">
<a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch>
<a-form-item label='Address Port Strategy'>
<a-select v-model="outbound.stream.sockopt.addressPortStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in Address_Port_Strategy" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Keep Alive Interval">
<a-input-number v-model="outbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
<a-input-number v-model.number="outbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP Fast Open">
<a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch>
</a-form-item>
<a-form-item label="Multipath TCP">
<a-switch v-model.trim="outbound.stream.sockopt.tcpMptcp"></a-switch>
</a-form-item>
<a-form-item label="TCP No-Delay" v-if="outbound.stream.sockopt.tcpMptcp">
<a-switch v-model="outbound.stream.sockopt.tcpNoDelay"></a-switch>
<a-form-item label="Penetrate">
<a-switch v-model="outbound.stream.sockopt.penetrate"></a-switch>
</a-form-item>
</template>
@@ -486,10 +491,10 @@
</a-form-item>
<template v-if="outbound.mux.enabled">
<a-form-item label="Concurrency">
<a-input-number v-model="outbound.mux.concurrency" :min="-1" :max="1024"></a-input-number>
<a-input-number v-model.number="outbound.mux.concurrency" :min="-1" :max="1024"></a-input-number>
</a-form-item>
<a-form-item label="xudp Concurrency">
<a-input-number v-model="outbound.mux.xudpConcurrency" :min="-1" :max="1024"></a-input-number>
<a-input-number v-model.number="outbound.mux.xudpConcurrency" :min="-1" :max="1024"></a-input-number>
</a-form-item>
<a-form-item label="xudp UDP 443">
<a-select v-model="outbound.mux.xudpProxyUDP443" :dropdown-class-name="themeSwitcher.currentTheme">
@@ -501,12 +506,12 @@
</a-form>
</a-tab-pane>
<a-tab-pane key="2" tab="JSON" force-render="true">
<a-form-item style="margin: 10px 0"> Link: <a-input v-model.trim="outModal.link" style="width: 300px; margin-right: 5px;" placeholder="vmess:// vless:// trojan:// ss://"></a-input>
<a-button @click="convertLink" type="primary">
<a-icon type="form"></a-icon>
</a-button>
</a-form-item>
<textarea style="position:absolute; left: -800px;" id="outboundJson"></textarea>
<a-space direction="vertical" :size="10" style="margin-top: 10px;">
<a-input addon-before='{{ i18n "pages.xray.outbound.link" }}' v-model.trim="outModal.link" placeholder="vmess:// vless:// trojan:// ss://">
<a-icon slot="addonAfter" type="form" @click="convertLink"></a-icon>
</a-input>
<textarea style="position:absolute; left: -800px;" id="outboundJson"></textarea>
</a-space>
</a-tab-pane>
</a-tabs>
{{end}}

View File

@@ -18,7 +18,7 @@
</table>
</a-collapse-panel>
</a-collapse>
<template v-if="inbound.isTcp && !inbound.stream.isReality">
<template v-if="inbound.isTcp">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
@@ -42,7 +42,7 @@
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label='xVer'>
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item>
</a-form>
<a-divider style="margin:5px 0;"></a-divider>

View File

@@ -18,7 +18,7 @@
</table>
</a-collapse-panel>
</a-collapse>
<template v-if="inbound.isTcp && !inbound.stream.isReality">
<template v-if="inbound.isTcp">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
@@ -42,7 +42,7 @@
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label='xVer'>
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item>
</a-form>
<a-divider style="margin:5px 0;"></a-divider>

View File

@@ -7,7 +7,7 @@
<a-input-number v-model.number="inbound.stream.reality.xver" :min="0"></a-input-number>
</a-form-item>
<a-form-item label='uTLS'>
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 50%"
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 100%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
@@ -43,11 +43,11 @@
<a-form-item label='SpiderX'>
<a-input v-model.trim="inbound.stream.reality.settings.spiderX"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input v-model.trim="inbound.stream.reality.privateKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input v-model.trim="inbound.stream.reality.settings.publicKey"></a-input>
<a-input v-model="inbound.stream.reality.settings.publicKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input type="password" v-model="inbound.stream.reality.privateKey"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>

View File

@@ -10,7 +10,7 @@
<a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button icon="plus" size="small" @click="inbound.stream.httpupgrade.addHeader('host', '')"></a-button>
<a-button icon="plus" size="small" @click="inbound.stream.httpupgrade.addHeader('', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.httpupgrade.headers">

View File

@@ -8,6 +8,7 @@
<a-select-option value="wechat-video">WeChat</a-select-option>
<a-select-option value="dtls">DTLS 1.2</a-select-option>
<a-select-option value="wireguard">WireGuard</a-select-option>
<a-select-option value="dns">DNS</a-select-option>
</a-select>
</a-form-item>
<a-form-item>

View File

@@ -6,22 +6,22 @@
</a-form-item>
<template v-if="inbound.stream.sockoptSwitch">
<a-form-item label="Route Mark">
<a-input-number v-model="inbound.stream.sockopt.mark" :min="0"></a-input-number>
<a-input-number v-model.number="inbound.stream.sockopt.mark" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP Keep Alive Interval">
<a-input-number v-model="inbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
<a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP Keep Alive Idle">
<a-input-number v-model="inbound.stream.sockopt.tcpKeepAliveIdle" :min="0"></a-input-number>
<a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveIdle" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP Max Seg">
<a-input-number v-model="inbound.stream.sockopt.tcpMaxSeg" :min="0"></a-input-number>
<a-input-number v-model.number="inbound.stream.sockopt.tcpMaxSeg" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP User Timeout">
<a-input-number v-model="inbound.stream.sockopt.tcpUserTimeout" :min="0"></a-input-number>
<a-input-number v-model.number="inbound.stream.sockopt.tcpUserTimeout" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP Window Clamp">
<a-input-number v-model="inbound.stream.sockopt.tcpWindowClamp" :min="0"></a-input-number>
<a-input-number v-model.number="inbound.stream.sockopt.tcpWindowClamp" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="Proxy Protocol">
<a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
@@ -32,8 +32,8 @@
<a-form-item label="Multipath TCP">
<a-switch v-model.trim="inbound.stream.sockopt.tcpMptcp"></a-switch>
</a-form-item>
<a-form-item label="TCP No-Delay" v-if="inbound.stream.sockopt.tcpMptcp">
<a-switch v-model.trim="inbound.stream.sockopt.tcpNoDelay"></a-switch>
<a-form-item label="Penetrate">
<a-switch v-model.trim="inbound.stream.sockopt.penetrate"></a-switch>
</a-form-item>
<a-form-item label="V6 Only">
<a-switch v-model.trim="inbound.stream.sockopt.V6Only"></a-switch>

View File

@@ -7,7 +7,7 @@
<a-input v-model.trim="inbound.stream.xhttp.path"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button icon="plus" size="small" @click="inbound.stream.xhttp.addHeader('host', '')"></a-button>
<a-button icon="plus" size="small" @click="inbound.stream.xhttp.addHeader('', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.xhttp.headers">
@@ -17,7 +17,7 @@
</a-input>
<a-input style="width: 50%" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button slot="addonAfter" size="small" @click="inbound.stream.xhttp.removeHeader(index)">-</a-button>
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.xhttp.removeHeader(index)"></a-button>
</a-input>
</a-input-group>
</a-form-item>
@@ -28,11 +28,14 @@
</a-select>
</a-form-item>
<a-form-item label="Max Buffered Upload" v-if="inbound.stream.xhttp.mode === 'packet-up'">
<a-input v-model.trim="inbound.stream.xhttp.scMaxBufferedPosts"></a-input>
<a-input-number v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"></a-input-number>
</a-form-item>
<a-form-item label="Max Upload Size (Byte)" v-if="inbound.stream.xhttp.mode === 'packet-up'">
<a-input v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input>
</a-form-item>
<a-form-item label="Stream-Up Server" v-if="inbound.stream.xhttp.mode === 'stream-up'">
<a-input v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
</a-form-item>
<a-form-item label="Padding Bytes">
<a-input v-model.trim="inbound.stream.xhttp.xPaddingBytes"></a-input>
</a-form-item>

View File

@@ -34,7 +34,7 @@
</a-input-group>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 50%"
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 100%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
@@ -57,6 +57,9 @@
<a-form-item label="Session Resumption">
<a-switch v-model="inbound.stream.tls.enableSessionResumption"></a-switch>
</a-form-item>
<a-form-item label="VerifyPeerCertInNames">
<a-input v-model.trim="inbound.stream.tls.verifyPeerCertInNames"></a-input>
</a-form-item>
<template v-for="cert,index in inbound.stream.tls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
@@ -82,10 +85,10 @@
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input type="textarea" :rows="3" v-model="cert.cert"></a-input>
<a-input v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input type="textarea" :rows="3" v-model="cert.key"></a-input>
<a-input type="password" v-model="cert.key"></a-input>
</a-form-item>
</template>
<a-form-item label='OCSP stapling'>

View File

@@ -55,18 +55,18 @@
<template slot="content" v-if="client.email">
<table cellpadding="2" width="100%">
<tr>
<td>↑[[ sizeFormat(getUpStats(record, client.email)) ]]</td>
<td>↓[[ sizeFormat(getDownStats(record, client.email)) ]]</td>
<td>↑[[ SizeFormatter.sizeFormat(getUpStats(record, client.email)) ]]</td>
<td>↓[[ SizeFormatter.sizeFormat(getDownStats(record, client.email)) ]]</td>
</tr>
<tr v-if="client.totalGB > 0">
<td>{{ i18n "remained" }}</td>
<td>[[ sizeFormat(getRemStats(record, client.email)) ]]</td>
<td>[[ SizeFormatter.sizeFormat(getRemStats(record, client.email)) ]]</td>
</tr>
</table>
</template>
<table>
<tr class="tr-table-box">
<td class="tr-table-rt"> [[ sizeFormat(getSumStats(record, client.email)) ]] </td>
<td class="tr-table-rt"> [[ SizeFormatter.sizeFormat(getSumStats(record, client.email)) ]] </td>
<td class="tr-table-bar" v-if="!client.enable">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
</td>
@@ -124,9 +124,9 @@
</template>
</span>
</template>
<a-tag style="min-width: 50px; border: none;" :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag>
<a-tag style="min-width: 50px; border: none;" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag>
</a-popover>
<a-tag v-else :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" style="border: none;" class="infinite-tag">
<a-tag v-else :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" style="border: none;" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
@@ -172,7 +172,7 @@
<td colspan="3" style="text-align: center;">{{ i18n "pages.inbounds.traffic" }}</td>
</tr>
<tr>
<td width="80px" style="margin:0; text-align: right;font-size: 1em;"> [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] </td>
<td width="80px" style="margin:0; text-align: right;font-size: 1em;"> [[ SizeFormatter.sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] </td>
<td width="120px" v-if="!client.enable">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
</td>
@@ -181,12 +181,12 @@
<template slot="content" v-if="client.email">
<table cellpadding="2" width="100%">
<tr>
<td>↑[[ sizeFormat(getUpStats(record, client.email)) ]]</td>
<td>↓[[ sizeFormat(getDownStats(record, client.email)) ]]</td>
<td>↑[[ SizeFormatter.sizeFormat(getUpStats(record, client.email)) ]]</td>
<td>↓[[ SizeFormatter.sizeFormat(getDownStats(record, client.email)) ]]</td>
</tr>
<tr>
<td>{{ i18n "remained" }}</td>
<td>[[ sizeFormat(getRemStats(record, client.email)) ]]</td>
<td>[[ SizeFormatter.sizeFormat(getRemStats(record, client.email)) ]]</td>
</tr>
</table>
</template>
@@ -244,7 +244,7 @@
</template>
</span>
</template>
<a-tag style="min-width: 50px; border: none;" :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag>
<a-tag style="min-width: 50px; border: none;" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag>
</a-popover>
<a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">

View File

@@ -181,12 +181,12 @@
<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>↑ [[ sizeFormat(infoModal.clientStats.up) ]] / [[ sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
<a-tag color="green">[[ SizeFormatter.sizeFormat(infoModal.clientStats.up + infoModal.clientStats.down) ]]</a-tag>
<a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up) ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
</td>
</tr>
<tr v-if="infoModal.clientSettings.comment">
<td>Comment</td>
<td>{{ i18n "comment" }}</td>
<td>
<a-tooltip :title="[[ infoModal.clientSettings.comment ]]">
<a-tag class="info-large-tag">[[ infoModal.clientSettings.comment ]]</a-tag>
@@ -224,7 +224,7 @@
<a-tag v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)"> [[ getRemStats() ]] </a-tag>
</td>
<td>
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)"> [[ sizeFormat(infoModal.clientSettings.totalGB) ]] </a-tag>
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)"> [[ SizeFormatter.sizeFormat(infoModal.clientSettings.totalGB) ]] </a-tag>
<a-tag v-else color="purple" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
@@ -233,7 +233,7 @@
</td>
<td>
<template v-if="infoModal.clientSettings.expiryTime > 0">
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
<a-tag :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
<template v-if="app.datepicker === 'gregorian'">
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
</template>
@@ -258,7 +258,7 @@
<tr-info-title class="tr-info-title">
<a-tag color="purple">Subscription Link</a-tag>
<a-tooltip title='{{ i18n "copy" }}'>
<a-button size="small" icon="snippets" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)"></a-button>
<a-button size="small" icon="snippets" @click="copy(infoModal.subLink)"></a-button>
</a-tooltip>
</tr-info-title>
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
@@ -267,7 +267,7 @@
<tr-info-title class="tr-info-title">
<a-tag color="purple">Json Link</a-tag>
<a-tooltip title='{{ i18n "copy" }}'>
<a-button size="small" icon="snippets" id="copy-subJson-link" @click="copyToClipboard('copy-subJson-link', infoModal.subJsonLink)"></a-button>
<a-button size="small" icon="snippets" @click="copy(infoModal.subJsonLink)"></a-button>
</a-tooltip>
</tr-info-title>
<a :href="[[ infoModal.subJsonLink ]]" target="_blank">[[ infoModal.subJsonLink ]]</a>
@@ -279,7 +279,7 @@
<tr-info-title class="tr-info-title">
<a-tag color="blue">[[ infoModal.clientSettings.tgId ]]</a-tag>
<a-tooltip title='{{ i18n "copy" }}'>
<a-button size="small" icon="snippets" id="copy-tg-link" @click="copyToClipboard('copy-tg-link', infoModal.clientSettings.tgId)"></a-button>
<a-button size="small" icon="snippets" @click="copy(infoModal.clientSettings.tgId)"></a-button>
</a-tooltip>
</tr-info-title>
</tr-info-row>
@@ -290,7 +290,7 @@
<tr-info-title class="tr-info-title">
<a-tag class="tr-info-tag" color="green">[[ link.remark ]]</a-tag>
<a-tooltip title='{{ i18n "copy" }}'>
<a-button style="min-width: 24px;" size="small" icon="snippets" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)"></a-button>
<a-button style="min-width: 24px;" size="small" icon="snippets" @click="copy(link.link)"></a-button>
</a-tooltip>
</tr-info-title>
<code>[[ link.link ]]</code>
@@ -304,7 +304,7 @@
<tr-info-title class="tr-info-title">
<a-tag class="tr-info-tag" color="green">[[ link.remark ]]</a-tag>
<a-tooltip title='{{ i18n "copy" }}'>
<a-button style="min-width: 24px;" size="small" icon="snippets" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)"></a-button>
<a-button style="min-width: 24px;" size="small" icon="snippets" @click="copy(link.link)"></a-button>
</a-tooltip>
</tr-info-title>
<code>[[ link.link ]]</code>
@@ -427,11 +427,11 @@
</tr>
<tr>
<td colspan="2">
<tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row">
<tr-info-row class="tr-info-row">
<tr-info-title class="tr-info-title">
<a-tag color="blue">Config</a-tag>
<a-tooltip title='{{ i18n "copy" }}'>
<a-button style="min-width: 24px;" size="small" icon="snippets" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, infoModal.links[index])"></a-button>
<a-button style="min-width: 24px;" size="small" icon="snippets" @click="copy(infoModal.links[index])"></a-button>
</a-tooltip>
</tr-info-title>
<div v-html="infoModal.links[index].replaceAll(`\n`,`<br />`)" style="border-radius: 1rem; padding: 0.5rem;" class="client-table-odd-row">
@@ -464,7 +464,6 @@
clientStats: [],
upStats: 0,
downStats: 0,
clipboard: null,
links: [],
index: null,
isExpired: false,
@@ -533,21 +532,19 @@
},
},
methods: {
copyToClipboard(elementId, content) {
this.infoModal.clipboard = new ClipboardJS('#' + elementId, {
text: () => content,
});
this.infoModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}')
this.infoModal.clipboard.destroy();
});
copy(content) {
ClipboardManager
.copyText(content)
.then(() => {
app.$message.success('{{ i18n "copied" }}')
})
},
statsColor(stats) {
return usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
return ColorUtils.usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
},
getRemStats() {
remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats.down;
return remained > 0 ? sizeFormat(remained) : '-';
return remained > 0 ? SizeFormatter.sizeFormat(remained) : '-';
},
refreshIPs() {
this.refreshing = true;

View File

@@ -43,6 +43,15 @@
margin:-10px 2px !important;
}
}
.dark .ant-switch-small:not(.ant-switch-checked) {
background-color: var(--dark-color-surface-100) !important;
}
.ant-custom-popover-title {
display: flex;
align-items: center;
gap: 10px;
margin: 5px 0;
}
.ant-col-sm-24 {
margin: 0.5rem -2rem 0.5rem 2rem;
}
@@ -137,406 +146,433 @@
</a-alert>
</transition>
<transition name="list" appear>
<a-card hoverable>
<a-card size="small" style="padding: 16px;" hoverable>
<a-row>
<a-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.totalDownUp" }}:
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
<a-col :sm="12" :md="6">
<a-custom-statistic title='{{ i18n "pages.inbounds.totalDownUp" }}' :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`">
<template #prefix>
<a-icon type="swap"></a-icon>
</template>
</a-custom-statistic>
</a-col>
<a-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.totalUsage" }}:
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
<a-col :sm="12" :md="6">
<a-custom-statistic title='{{ i18n "pages.inbounds.totalUsage" }}' :value="SizeFormatter.sizeFormat(total.up + total.down)" :style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix>
<a-icon type="pie-chart"></a-icon>
</template>
</a-custom-statistic>
</a-col>
<a-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.inboundCount" }}:
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
<a-col :sm="12" :md="6">
<a-custom-statistic title='{{ i18n "pages.inbounds.inboundCount" }}' :value="dbInbounds.length" :style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix>
<a-icon type="bars"></a-icon>
</template>
</a-custom-statistic>
</a-col>
<a-col :xs="24" :sm="24" :lg="12">
<template>
<div>
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
{{ i18n "clients" }}:
<a-tag color="green">[[ total.clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
</template>
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
</template>
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
</template>
<a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in onlineClients">[[ clientEmail ]]</p>
</template>
<a-tag color="blue" v-if="onlineClients.length">[[ onlineClients.length ]]</a-tag>
</a-popover>
</div>
</template>
<a-col :sm="12" :md="6">
<a-custom-statistic title='{{ i18n "clients" }}' value=" " :style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix>
<a-space direction="horizontal">
<a-icon type="team"></a-icon>
<div>
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
<a-tag color="green">[[ total.clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<div v-for="clientEmail in total.deactive"><span>[[ clientEmail ]]</span></div>
</template>
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<div v-for="clientEmail in total.depleted"><span>[[ clientEmail ]]</span></div>
</template>
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<div v-for="clientEmail in total.expiring"><span>[[ clientEmail ]]</span></div>
</template>
<a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<div v-for="clientEmail in onlineClients"><span>[[ clientEmail ]]</span></div>
</template>
<a-tag color="blue" v-if="onlineClients.length">[[ onlineClients.length ]]</a-tag>
</a-popover>
</div>
</a-space>
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</transition>
<transition name="list" appear>
<a-card hoverable>
<div slot="title">
<a-row>
<a-col :xs="12" :sm="12" :lg="12">
<a-button type="primary" icon="plus" @click="openAddInbound">
<template v-if="!isMobile">{{ i18n "pages.inbounds.addInbound" }}</template>
</a-button>
<a-dropdown :trigger="['click']">
<a-button type="primary" icon="menu">
<template v-if="!isMobile">{{ i18n "pages.inbounds.generalActions" }}</template>
</a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
<a-menu-item key="import">
<a-icon type="import"></a-icon>
{{ i18n "pages.inbounds.importInbound" }}
</a-menu-item>
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }}
</a-menu-item>
<a-menu-item key="subs" v-if="subSettings.enable">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }} - {{ i18n "pages.settings.subSettings" }}
</a-menu-item>
<a-menu-item key="resetInbounds">
<a-icon type="reload"></a-icon>
{{ i18n "pages.inbounds.resetAllTraffic" }}
</a-menu-item>
<a-menu-item key="resetClients">
<a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
</a-menu-item>
<a-menu-item key="delDepletedClients" style="color: #FF4D4F;">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
</a-menu>
</a-dropdown>
</a-col>
<a-col :xs="12" :sm="12" :lg="12" style="text-align: right;">
<a-select v-model="refreshInterval"
style="width: 65px;"
v-if="isRefreshEnabled"
@change="changeRefreshInterval"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
</a-select>
<a-icon type="sync" :spin="refreshing" @click="manualRefresh" style="margin: 0 5px;"></a-icon>
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch>
</a-col>
</a-row>
</div>
<div :style="isMobile ? '' : 'display: flex; align-items: center; justify-content: flex-start;'">
<a-switch v-model="enableFilter"
:style="isMobile ? 'margin-bottom: .5rem; display: flex;' : 'margin-right: .5rem;'"
@change="toggleFilter">
<a-icon slot="checkedChildren" type="search"></a-icon>
<a-icon slot="unCheckedChildren" type="filter"></a-icon>
</a-switch>
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px" :size="isMobile ? 'small' : ''"></a-input>
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid" :size="isMobile ? 'small' : ''">
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
<a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
<a-radio-button value="online">{{ i18n "online" }}</a-radio-button>
</a-radio-group>
</div>
<a-back-top></a-back-top>
<a-table :columns="isMobile ? mobileColumns : columns" :row-key="dbInbound => dbInbound.id"
:data-source="searchedInbounds"
:scroll="isMobile ? {} : { x: 1000 }"
:pagination=pagination(searchedInbounds)
:expand-icon-as-cell="false"
:expand-row-by-click="false"
:expand-icon-column-index="0"
:indent-size="0"
:row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
style="margin-top: 10px">
<template slot="action" slot-scope="text, dbInbound">
<template #title>
<a-space direction="horizontal">
<a-button type="primary" icon="plus" @click="openAddInbound">
<template v-if="!isMobile">{{ i18n "pages.inbounds.addInbound" }}</template>
</a-button>
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 20px; text-decoration: solid;"></a-icon>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
<a-menu-item key="edit">
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
<a-button type="primary" icon="menu">
<template v-if="!isMobile">{{ i18n "pages.inbounds.generalActions" }}</template>
</a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
<a-menu-item key="import">
<a-icon type="import"></a-icon>
{{ i18n "pages.inbounds.importInbound" }}
</a-menu-item>
<a-menu-item key="qrcode" v-if="(dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser) || dbInbound.isWireguard">
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }}
</a-menu-item>
<template v-if="dbInbound.isMultiUser()">
<a-menu-item key="addClient">
<a-icon type="user-add"></a-icon>
{{ i18n "pages.client.add"}}
</a-menu-item>
<a-menu-item key="addBulkClient">
<a-icon type="usergroup-add"></a-icon>
{{ i18n "pages.client.bulk"}}
</a-menu-item>
<a-menu-item key="resetClients">
<a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetInboundClientTraffics"}}
</a-menu-item>
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export"}}
</a-menu-item>
<a-menu-item key="subs" v-if="subSettings.enable">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}
</a-menu-item>
<a-menu-item key="delDepletedClients" style="color: #FF4D4F;">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
</template>
<template v-else>
<a-menu-item key="showInfo">
<a-icon type="info-circle"></a-icon>
{{ i18n "info"}}
</a-menu-item>
</template>
<a-menu-item key="clipboard">
<a-icon type="copy"></a-icon>
{{ i18n "pages.inbounds.exportInbound" }}
<a-menu-item key="subs" v-if="subSettings.enable">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }} - {{ i18n "pages.settings.subSettings" }}
</a-menu-item>
<a-menu-item key="resetTraffic">
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
<a-menu-item key="resetInbounds">
<a-icon type="reload"></a-icon>
{{ i18n "pages.inbounds.resetAllTraffic" }}
</a-menu-item>
<a-menu-item key="clone">
<a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}}
<a-menu-item key="resetClients">
<a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
</a-menu-item>
<a-menu-item key="delete">
<span style="color: #FF4D4F">
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
</span>
</a-menu-item>
<a-menu-item v-if="isMobile">
<a-switch size="small" v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
{{ i18n "pages.inbounds.enable" }}
<a-menu-item key="delDepletedClients" style="color: #FF4D4F;">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
<template slot="protocol" slot-scope="text, dbInbound">
<a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="blue">TLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="blue">Reality</a-tag>
</a-space>
</template>
<template #extra>
<a-button-group>
<a-button icon="sync" @click="manualRefresh" :loading="refreshing"></a-button>
<a-popover placement="bottomRight" trigger="click" :overlay-class-name="themeSwitcher.currentTheme">
<template #title>
<div class="ant-custom-popover-title">
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh" size="small"></a-switch>
<span>{{ i18n "pages.inbounds.autoRefresh" }}</span>
</div>
</template>
<template #content>
<a-space direction="vertical">
<span>{{ i18n "pages.inbounds.autoRefreshInterval" }}</span>
<a-select v-model="refreshInterval"
:disabled="!isRefreshEnabled"
style="width: 100%;"
@change="changeRefreshInterval"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
</a-select>
</a-space>
</template>
<a-button icon="down"></a-button>
</a-popover>
</a-button-group>
</template>
<a-space direction="vertical">
<div :style="isMobile ? '' : 'display: flex; align-items: center; justify-content: flex-start;'">
<a-switch v-model="enableFilter"
:style="isMobile ? 'margin-bottom: .5rem; display: flex;' : 'margin-right: .5rem;'"
@change="toggleFilter">
<a-icon slot="checkedChildren" type="search"></a-icon>
<a-icon slot="unCheckedChildren" type="filter"></a-icon>
</a-switch>
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px" :size="isMobile ? 'small' : ''"></a-input>
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid" :size="isMobile ? 'small' : ''">
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
<a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
<a-radio-button value="online">{{ i18n "online" }}</a-radio-button>
</a-radio-group>
</div>
<a-table :columns="isMobile ? mobileColumns : columns" :row-key="dbInbound => dbInbound.id"
:data-source="searchedInbounds"
:scroll="isMobile ? {} : { x: 1000 }"
:pagination=pagination(searchedInbounds)
:expand-icon-as-cell="false"
:expand-row-by-click="false"
:expand-icon-column-index="0"
:indent-size="0"
:row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
style="margin-top: 10px">
<template slot="action" slot-scope="text, dbInbound">
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 20px; text-decoration: solid;"></a-icon>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
<a-menu-item key="edit">
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<a-menu-item key="qrcode" v-if="(dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser) || dbInbound.isWireguard">
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
</a-menu-item>
<template v-if="dbInbound.isMultiUser()">
<a-menu-item key="addClient">
<a-icon type="user-add"></a-icon>
{{ i18n "pages.client.add"}}
</a-menu-item>
<a-menu-item key="addBulkClient">
<a-icon type="usergroup-add"></a-icon>
{{ i18n "pages.client.bulk"}}
</a-menu-item>
<a-menu-item key="resetClients">
<a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetInboundClientTraffics"}}
</a-menu-item>
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export"}}
</a-menu-item>
<a-menu-item key="subs" v-if="subSettings.enable">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}
</a-menu-item>
<a-menu-item key="delDepletedClients" style="color: #FF4D4F;">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
</template>
<template v-else>
<a-menu-item key="showInfo">
<a-icon type="info-circle"></a-icon>
{{ i18n "info"}}
</a-menu-item>
</template>
<a-menu-item key="clipboard">
<a-icon type="copy"></a-icon>
{{ i18n "pages.inbounds.exportInbound" }}
</a-menu-item>
<a-menu-item key="resetTraffic">
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
</a-menu-item>
<a-menu-item key="clone">
<a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}}
</a-menu-item>
<a-menu-item key="delete">
<span style="color: #FF4D4F">
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
</span>
</a-menu-item>
<a-menu-item v-if="isMobile">
<a-switch size="small" v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
{{ i18n "pages.inbounds.enable" }}
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
</template>
<template slot="clients" slot-scope="text, dbInbound">
<template v-if="clientCount[dbInbound.id]">
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="protocol" slot-scope="text, dbInbound">
<a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="blue">TLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="blue">Reality</a-tag>
</template>
</template>
<template slot="clients" slot-scope="text, dbInbound">
<template v-if="clientCount[dbInbound.id]">
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<div v-for="clientEmail in clientCount[dbInbound.id].deactive"><span>[[ clientEmail ]]</span></div>
</template>
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<div v-for="clientEmail in clientCount[dbInbound.id].depleted"><span>[[ clientEmail ]]</span></div>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<div v-for="clientEmail in clientCount[dbInbound.id].expiring"><span>[[ clientEmail ]]</span></div>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<div v-for="clientEmail in clientCount[dbInbound.id].online"><span>[[ clientEmail ]]</span></div>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="blue" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
</a-popover>
</template>
</template>
<template slot="traffic" slot-scope="text, dbInbound">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
<table cellpadding="2" width="100%">
<tr>
<td>↑[[ SizeFormatter.sizeFormat(dbInbound.up) ]]</td>
<td>↓[[ SizeFormatter.sizeFormat(dbInbound.down) ]]</td>
</tr>
<tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
<td>{{ i18n "remained" }}</td>
<td>[[ SizeFormatter.sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
</tr>
</table>
</template>
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="blue" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
<a-tag :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
[[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] /
<template v-if="dbInbound.total > 0">
[[ SizeFormatter.sizeFormat(dbInbound.total) ]]
</template>
<template v-else>
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
</template>
</a-tag>
</a-popover>
</template>
</template>
<template slot="traffic" slot-scope="text, dbInbound">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<table cellpadding="2" width="100%">
<tr>
<td>↑[[ sizeFormat(dbInbound.up) ]]</td>
<td>↓[[ sizeFormat(dbInbound.down) ]]</td>
</tr>
<tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
<td>{{ i18n "remained" }}</td>
<td>[[ sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
</tr>
</table>
</template>
<a-tag :color="usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
[[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
<template v-if="dbInbound.total > 0">
[[ sizeFormat(dbInbound.total) ]]
<template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
</template>
<template slot="expiryTime" slot-scope="text, dbInbound">
<a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content" v-if="app.datepicker === 'gregorian'">
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
</template>
<template v-else>
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
<template v-else slot="content">
[[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
</template>
<a-tag style="min-width: 50px;" :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
[[ remainedDays(dbInbound._expiryTime) ]]
</a-tag>
</a-popover>
<a-tag v-else color="purple" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
</a-tag>
</a-popover>
</template>
<template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
</template>
<template slot="expiryTime" slot-scope="text, dbInbound">
<a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content" v-if="app.datepicker === 'gregorian'">
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
</template>
<template v-else slot="content">
[[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
</template>
<a-tag style="min-width: 50px;" :color="usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
[[ remainedDays(dbInbound._expiryTime) ]]
</a-tag>
</a-popover>
<a-tag v-else color="purple" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
</a-tag>
</template>
<template slot="info" slot-scope="text, dbInbound">
<a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
<template slot="content">
<table cellpadding="2">
<tr>
<td>{{ i18n "pages.inbounds.protocol" }}</td>
<td>
<a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag style="margin:0;" color="blue">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="green">tls</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="green">reality</a-tag>
</template>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.port" }}</td>
<td><a-tag>[[ dbInbound.port ]]</a-tag></td>
</tr>
<tr v-if="clientCount[dbInbound.id]">
<td>{{ i18n "clients" }}</td>
<td>
<a-tag style="margin:0;" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
</template>
<template slot="info" slot-scope="text, dbInbound">
<a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
<template slot="content">
<table cellpadding="2">
<tr>
<td>{{ i18n "pages.inbounds.protocol" }}</td>
<td>
<a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag style="margin:0;" color="blue">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="green">tls</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="green">reality</a-tag>
</template>
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
</a-popover>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.traffic" }}</td>
<td>
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<table cellpadding="2" width="100%">
<tr>
<td>↑[[ sizeFormat(dbInbound.up) ]]</td>
<td>↓[[ sizeFormat(dbInbound.down) ]]</td>
</tr>
<tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
<td>{{ i18n "remained" }}</td>
<td>[[ sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
</tr>
</table>
</template>
<a-tag :color="usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
[[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
<template v-if="dbInbound.total > 0">
[[ sizeFormat(dbInbound.total) ]]
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.port" }}</td>
<td><a-tag>[[ dbInbound.port ]]</a-tag></td>
</tr>
<tr v-if="clientCount[dbInbound.id]">
<td>{{ i18n "clients" }}</td>
<td>
<a-tag style="margin:0;" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
</a-popover>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.traffic" }}</td>
<td>
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<table cellpadding="2" width="100%">
<tr>
<td>↑[[ SizeFormatter.sizeFormat(dbInbound.up) ]]</td>
<td>↓[[ SizeFormatter.sizeFormat(dbInbound.down) ]]</td>
</tr>
<tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
<td>{{ i18n "remained" }}</td>
<td>[[ SizeFormatter.sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
</tr>
</table>
</template>
<a-tag :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
[[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] /
<template v-if="dbInbound.total > 0">
[[ SizeFormatter.sizeFormat(dbInbound.total) ]]
</template>
<template v-else>
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
</template>
</a-tag>
</a-popover>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.expireDate" }}</td>
<td>
<a-tag style="min-width: 50px; text-align: center;" v-if="dbInbound.expiryTime > 0"
:color="dbInbound.isExpiry? 'red': 'blue'">
<template v-if="app.datepicker === 'gregorian'">
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
</template>
<template v-else>
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
[[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
</template>
</a-tag>
</a-popover>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.expireDate" }}</td>
<td>
<a-tag style="min-width: 50px; text-align: center;" v-if="dbInbound.expiryTime > 0"
:color="dbInbound.isExpiry? 'red': 'blue'">
<template v-if="app.datepicker === 'gregorian'">
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
</template>
<template v-else>
[[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
</template>
</a-tag>
<a-tag v-else style="text-align: center;" color="purple" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
</a-tag>
</td>
</tr>
</table>
</template>
<a-badge>
<a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle" :style="'color: ' + themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-icon>
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;">
<a-icon type="info"></a-icon>
</a-button>
</a-badge>
</a-popover>
</template>
<template slot="expandedRowRender" slot-scope="record">
<a-table
:row-key="client => client.id"
:columns="isMobile ? innerMobileColumns : innerColumns"
:data-source="getInboundClients(record)"
:pagination=pagination(getInboundClients(record))
:style="isMobile ? 'margin: -10px 2px -11px;' : 'margin: -10px 22px -11px;'">
{{template "client_table"}}
</a-table>
</template>
</a-table>
<a-tag v-else style="text-align: center;" color="purple" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
</a-tag>
</td>
</tr>
</table>
</template>
<a-badge>
<a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle" :style="'color: ' + themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-icon>
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;">
<a-icon type="info"></a-icon>
</a-button>
</a-badge>
</a-popover>
</template>
<template slot="expandedRowRender" slot-scope="record">
<a-table
:row-key="client => client.id"
:columns="isMobile ? innerMobileColumns : innerColumns"
:data-source="getInboundClients(record)"
:pagination=pagination(getInboundClients(record))
:style="isMobile ? 'margin: -10px 2px -11px;' : 'margin: -10px 22px -11px;'">
{{template "client_table"}}
</a-table>
</template>
</a-table>
</a-space>
</a-card>
</transition>
</a-spin>
@@ -544,14 +580,13 @@
</a-layout>
</a-layout>
{{template "js" .}}
<script src="{{ .base_path }}assets/base64/base64.min.js"></script>
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/clipboard/clipboard.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
{{template "component/themeSwitcher" .}}
{{template "component/persianDatepicker" .}}
{{template "component/aThemeSwitch" .}}
{{template "component/aCustomStatistic" .}}
{{template "component/aPersianDatepicker" .}}
<script>
const columns = [{
title: "ID",
@@ -664,6 +699,7 @@
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
subSettings: {
enable : false,
subTitle : '',
subURI : '',
subJsonURI : '',
},
@@ -713,6 +749,7 @@
this.tgBotEnable = tgBotEnable;
this.subSettings = {
enable : subEnable,
subTitle : subTitle,
subURI: subURI,
subJsonURI: subJsonURI
};
@@ -885,7 +922,7 @@
this.exportSubs(dbInbound.id);
break;
case "clipboard":
this.copyToClipboard(dbInbound.id);
this.copy(dbInbound.id);
break;
case "resetTraffic":
this.resetTraffic(dbInbound.id);
@@ -929,7 +966,7 @@
expiryTime: dbInbound.expiryTime,
listen: '',
port: RandomUtil.randomIntRange(10000, 60000),
port: RandomUtil.randomInteger(10000, 60000),
protocol: baseInbound.protocol,
settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
streamSettings: baseInbound.stream.toString(),
@@ -1108,6 +1145,36 @@
this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
}
},
getSubGroupClients(dbInbounds, currentClient) {
const response = {
inbounds: [],
clients: [],
editIds: []
}
if (dbInbounds && dbInbounds.length > 0 && currentClient) {
dbInbounds.forEach((dbInboundItem) => {
const dbInbound = new DBInbound(dbInboundItem);
if (dbInbound) {
const inbound = dbInbound.toInbound();
if (inbound) {
const clients = inbound.clients;
if (clients.length > 0) {
clients.forEach((client) => {
if (client['subId'] === currentClient['subId']) {
client['inboundId'] = dbInboundItem.id
client['clientId'] = this.getClientId(dbInbound.protocol, client)
response.inbounds.push(dbInboundItem.id)
response.clients.push(client)
response.editIds.push(client['clientId'])
}
})
}
}
}
})
}
return response;
},
getClientId(protocol, client) {
switch (protocol) {
case Protocols.TROJAN: return client.password;
@@ -1246,9 +1313,9 @@
return remained>0 ? remained : 0;
},
clientStatsColor(dbInbound, email) {
if (email.length == 0) return clientUsageColor();
if (email.length == 0) return ColorUtils.clientUsageColor();
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return clientUsageColor(clientStats, app.trafficDiff)
return ColorUtils.clientUsageColor(clientStats, app.trafficDiff)
},
statsProgress(dbInbound, email) {
if (email.length == 0) return 100;
@@ -1266,17 +1333,17 @@
},
remainedDays(expTime){
if (expTime == 0) return null;
if (expTime < 0) return formatSecond(expTime/-1000);
if (expTime < 0) return TimeFormatter.formatSecond(expTime/-1000);
now = new Date().getTime();
if (expTime < now) return '{{ i18n "depleted" }}';
return formatSecond((expTime-now)/1000);
return TimeFormatter.formatSecond((expTime-now)/1000);
},
statsExpColor(dbInbound, email){
if (email.length == 0) return '#7a316f';
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
if (!clientStats) return '#7a316f';
statsColor = usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
expColor = usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime);
statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime);
switch (true) {
case statsColor == "red" || expColor == "red":
return "#cf3c3c"; // Red
@@ -1310,7 +1377,7 @@
if (clients != null){
clients.forEach(c => {
if (c.subId && c.subId.length>0){
subLinks.push(this.subSettings.subURI + c.subId + "?name=" + c.subId)
subLinks.push(this.subSettings.subURI + c.subId)
}
})
}
@@ -1337,7 +1404,7 @@
if (clients != null){
clients.forEach(c => {
if (c.subId && c.subId.length>0){
subLinks.push(this.subSettings.subURI + c.subId + "?name=" + c.subId)
subLinks.push(this.subSettings.subURI + c.subId)
}
})
}
@@ -1354,9 +1421,9 @@
}
txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText.join('\r\n'), 'All-Inbounds');
},
copyToClipboard(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
txtModal.show('{{ i18n "pages.inbounds.inboundData" }}', JSON.stringify(dbInbound, null, 2));
copy(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
txtModal.show('{{ i18n "pages.inbounds.inboundData" }}', JSON.stringify(dbInbound, null, 2));
},
async startDataRefreshLoop() {
while (this.isRefreshEnabled) {
@@ -1410,7 +1477,7 @@
}
},
watch: {
searchKey: debounce(function (newVal) {
searchKey: Utils.debounce(function (newVal) {
this.searchInbounds(newVal);
}, 500)
},

View File

@@ -19,6 +19,57 @@
.ant-card-dark h2 {
color: var(--dark-color-text-primary);
}
.ant-backup-list-item {
gap: 10px;
}
.dark .ant-backup-list-item svg,
.dark .ant-badge-status-text,
.dark .ant-card-extra {
color: var(--dark-color-text-primary);
}
.dark .ant-card-actions>li {
color: rgba(255, 255, 255, 0.55);
}
.dark .ant-radio-inner {
background-color: var(--dark-color-surface-100);
border-color: var(--dark-color-surface-600);
}
.dark .ant-radio-checked .ant-radio-inner {
border-color: var(--color-primary-100);
}
.dark .ant-backup-list,
.dark .ant-xray-version-list,
.dark .ant-card-actions,
.dark .ant-card-actions>li:not(:last-child) {
border-color: var(--dark-color-stroke);
}
.ant-card-actions {
background: transparent;
}
.ip-hidden {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
filter: blur(10px);
}
.running-animation .ant-badge-status-dot {
animation: runningAnimation 1.2s linear infinite;
}
.running-animation .ant-badge-status-processing:after {
border-color: var(--color-primary-100);
}
@keyframes runningAnimation {
0%,
50%,
100% {
transform: scale(1);
opacity: 1;
}
10% {
transform: scale(1.5);
opacity: .2;
}
}
</style>
<body>
@@ -36,238 +87,265 @@
</a-alert>
</transition>
<transition name="list" appear>
<a-row>
<a-card hoverable>
<template>
<a-row v-if="!status.isLoaded">
<a-card hoverable style="text-align: center; padding: 30px 0; margin-top: 10px; background: transparent;">
<a-spin tip="Loading..."></a-spin>
</a-card>
</a-row>
<a-row v-else>
<a-row>
<a-col :sm="24" :md="12">
<a-card hoverable>
<a-row>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color"
:percent="status.cpu.percent"></a-progress>
<div><b>CPU:</b> [[ cpuCoreFormat(status.cpuCores) ]] <a-tooltip>
<a-icon type="area-chart"></a-icon>
<template slot="title">
<div><b>Logical Processors:</b> [[ (status.logicalPro) ]]</div>
<div><b>Speed:</b> [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
</template>
</a-tooltip></div>
<a-col :sm="24" :md="12">
<a-row>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color"
:percent="status.cpu.percent"></a-progress>
<div><b>CPU:</b> [[ CPUFormatter.cpuCoreFormat(status.cpuCores) ]] <a-tooltip>
<a-icon type="area-chart"></a-icon>
<template slot="title">
<div><b>Logical Processors:</b> [[ (status.logicalPro) ]]</div>
<div><b>Speed:</b> [[ CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
</template>
</a-tooltip></div>
</a-col>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color"
:percent="status.mem.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.memory"}}:</b> [[ SizeFormatter.sizeFormat(status.mem.current) ]] / [[ SizeFormatter.sizeFormat(status.mem.total) ]]
</div>
</a-col>
</a-row>
</a-col>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color"
:percent="status.mem.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.memory"}}:</b> [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
</div>
<a-col :sm="24" :md="12">
<a-row>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color"
:percent="status.swap.percent"></a-progress>
<div>
<b>Swap:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] / [[ SizeFormatter.sizeFormat(status.swap.total) ]]
</div>
</a-col>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color"
:percent="status.disk.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.hard"}}:</b> [[ SizeFormatter.sizeFormat(status.disk.current) ]] / [[ SizeFormatter.sizeFormat(status.disk.total) ]]
</div>
</a-col>
</a-row>
</a-col>
</a-row>
</a-col>
<a-col :sm="24" :md="12">
<a-row>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color"
:percent="status.swap.percent"></a-progress>
<div>
<b>Swap:</b> [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
</div>
</a-col>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color"
:percent="status.disk.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.hard"}}:</b> [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
</div>
</a-col>
</a-row>
</a-col>
</a-card>
</a-row>
</a-card>
</a-row>
</transition>
<transition name="list" appear>
<a-row>
<a-col :sm="24" :lg="12">
<a-card hoverable>
<b>3X-UI:</b>
<a rel="noopener" href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
<a rel="noopener" href="https://t.me/XrayUI" target="_blank"><a-tag color="green">@XrayUI</a-tag></a>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card hoverable>
<b>{{ i18n "pages.index.operationHours" }}:</b>
<a-tag :color="status.xray.color">Xray: [[ formatSecond(status.appStats.uptime) ]]</a-tag>
<a-tag color="green">OS: [[ formatSecond(status.uptime) ]]</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card hoverable>
<b>{{ i18n "pages.index.xrayStatus" }}:</b>
<a-tag style="text-transform: capitalize;" :color="status.xray.color">[[ status.xray.state ]] </a-tag>
<a-popover v-if="status.xray.state === State.Error" :overlay-class-name="themeSwitcher.currentTheme">
<span slot="title" style="font-size: 12pt">An error occurred while running Xray
<a-tag color="purple" style="cursor: pointer; float: right;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
</span>
<template slot="content">
<p style="max-width: 400px" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
<a-col :sm="24" :lg="12">
<a-card hoverable>
<template #title>
<a-space direction="horizontal">
<span>{{ i18n "pages.index.xrayStatus" }}</span>
<a-tag v-if="isMobile && status.xray.version != 'Unknown'" color="green">
v[[ status.xray.version ]]
</a-tag>
</a-space>
</template>
<a-icon type="question-circle"></a-icon>
</a-popover>
<a-tag color="purple" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
<a-tag color="purple" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
<a-tag color="purple" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card hoverable>
<b>{{ i18n "menu.link" }}:</b>
<a-tag color="purple" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
<a-tag color="purple" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
<a-tag color="purple" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card hoverable>
<b>{{ i18n "pages.index.systemLoad" }}:</b>
<a-tag color="green">
<a-tooltip>
[[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<template slot="title">
{{ i18n "pages.index.systemLoadDesc" }}
<template #extra>
<template v-if="status.xray.state != State.Error">
<a-badge status="processing" class="running-animation" :text="status.xray.state" :color="status.xray.color" style="text-transform: capitalize;"/>
</template>
</a-tooltip>
</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card hoverable>
<b>{{ i18n "usage"}}:</b>
<a-tag color="green"> RAM: [[ sizeFormat(status.appStats.mem) ]] </a-tag>
<a-tag color="green"> Threads: [[ status.appStats.threads ]] </a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card hoverable>
<a-row>
<a-col :span="12">
<a-tag>
<a-tooltip>
<a-icon type="global"></a-icon> IPv4
<template slot="title">
[[ status.publicIP.ipv4 ]]
<template v-else>
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<span slot="title" style="font-size: 12pt">An error occurred while running Xray
<a-tag color="purple" style="cursor: pointer; float: right;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
</span>
<template slot="content">
<p style="max-width: 400px" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
</template>
</a-tooltip>
</a-tag>
</a-col>
<a-col :span="12">
<a-tag>
<a-tooltip>
<a-icon type="global"></a-icon> IPv6
<template slot="title">
[[ status.publicIP.ipv6 ]]
<a-badge :text="status.xray.state" :color="status.xray.color" style="text-transform: capitalize;"/>
</a-popover>
</template>
</template>
<template #actions>
<a-space direction="horizontal" @click="stopXrayService" style="justify-content: center;">
<a-icon type="poweroff"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
</a-space>
<a-space direction="horizontal" @click="restartXrayService" style="justify-content: center;">
<a-icon type="reload"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.restartXray" }}</span>
</a-space>
<a-space direction="horizontal" @click="openSelectV2rayVersion" style="justify-content: center;">
<a-icon type="tool"></a-icon>
<span v-if="!isMobile">
[[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n "pages.index.xraySwitch" }}' ]]
</span>
</a-space>
</template>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "menu.link" }}' hoverable>
<template #actions>
<a-space direction="horizontal" @click="openLogs()" style="justify-content: center;">
<a-icon type="bars"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
</a-space>
<a-space direction="horizontal" @click="openConfig" style="justify-content: center;">
<a-icon type="control"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.config" }}</span>
</a-space>
<a-space direction="horizontal" @click="openBackup" style="justify-content: center;">
<a-icon type="cloud-server"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.backup" }}</span>
</a-space>
</template>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='3X-UI' hoverable>
<a rel="noopener" href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
<a rel="noopener" href="https://t.me/XrayUI" target="_blank"><a-tag color="green">@XrayUI</a-tag></a>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.operationHours" }}' hoverable>
<a-tag :color="status.xray.color">Xray: [[ TimeFormatter.formatSecond(status.appStats.uptime) ]]</a-tag>
<a-tag color="green">OS: [[ TimeFormatter.formatSecond(status.uptime) ]]</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.systemLoad" }}' hoverable>
<a-tag color="green">
<a-tooltip>
[[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<template slot="title">
{{ i18n "pages.index.systemLoadDesc" }}
</template>
</a-tooltip>
</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "usage"}}' hoverable>
<a-tag color="green"> RAM: [[ SizeFormatter.sizeFormat(status.appStats.mem) ]] </a-tag>
<a-tag color="green"> Threads: [[ status.appStats.threads ]] </a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.overallSpeed" }}' hoverable>
<a-row>
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.upload" }}' :value="SizeFormatter.sizeFormat(status.netIO.up)">
<template #prefix>
<a-icon type="arrow-up" />
</template>
</a-tooltip>
</a-tag>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card hoverable>
<a-row>
<a-col :span="12">
<a-tag>
<a-tooltip>
<a-icon type="swap"></a-icon> TCP: [[ status.tcpCount ]]
<template slot="title">
{{ i18n "pages.index.connectionTcpCountDesc" }}
<template #suffix>
/s
</template>
</a-tooltip>
</a-tag>
</a-col>
<a-col :span="12">
<a-tag>
<a-tooltip>
<a-icon type="swap"></a-icon> UDP: [[ status.udpCount ]]
<template slot="title">
{{ i18n "pages.index.connectionUdpCountDesc" }}
</a-custom-statistic>
</a-col>
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.download" }}' :value="SizeFormatter.sizeFormat(status.netIO.down)">
<template #prefix>
<a-icon type="arrow-down" />
</template>
</a-tooltip>
</a-tag>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card hoverable>
<a-row>
<a-col :span="12">
<a-tag>
<a-tooltip>
<a-icon type="arrow-up"></a-icon> Up: [[ sizeFormat(status.netIO.up) ]]/s
<template slot="title">
{{ i18n "pages.index.upSpeed" }}
<template #suffix>
/s
</template>
</a-tooltip>
</a-tag>
</a-col>
<a-col :span="12">
<a-tag>
<a-tooltip>
<a-icon type="arrow-down"></a-icon> Down: [[ sizeFormat(status.netIO.down) ]]/s
<template slot="title">
{{ i18n "pages.index.downSpeed" }}
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.totalData" }}' hoverable>
<a-row>
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.sent" }}' :value="SizeFormatter.sizeFormat(status.netTraffic.sent)">
<template #prefix>
<a-icon type="cloud-upload" />
</template>
</a-tooltip>
</a-tag>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card hoverable>
<a-row>
<a-col :span="12">
<a-tag>
<a-tooltip>
<a-icon type="cloud-upload"></a-icon>
<template slot="title">
{{ i18n "pages.index.totalSent" }}
</template> Out: [[ sizeFormat(status.netTraffic.sent) ]]
</a-tooltip>
</a-tag>
</a-col>
<a-col :span="12">
<a-tag>
<a-tooltip>
<a-icon type="cloud-download"></a-icon>
<template slot="title">
{{ i18n "pages.index.totalReceive" }}
</template> In: [[ sizeFormat(status.netTraffic.recv) ]]
</a-tooltip>
</a-tag>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
</a-custom-statistic>
</a-col>
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.received" }}' :value="SizeFormatter.sizeFormat(status.netTraffic.recv)">
<template #prefix>
<a-icon type="cloud-download" />
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.ipAddresses" }}' hoverable>
<template #extra>
<a-tooltip>
<template #title>
{{ i18n "pages.index.toggleIpVisibility" }}
</template>
<a-icon :type="showIp ? 'eye' : 'eye-invisible'" :style="{ fontSize: '1rem' }" @click="showIp = !showIp"></a-icon>
</a-tooltip>
</template>
<a-row :class="showIp ? 'ip-visible' : 'ip-hidden'">
<a-col :xs="24" :xxl="12" :style="{ marginTop: isMobile ? '10px' : 0 }">
<a-custom-statistic title="IPv4" :value="status.publicIP.ipv4">
<template #prefix>
<a-icon type="global" />
</template>
</a-custom-statistic>
</a-col>
<a-col :xs="24" :xxl="12" :style="{ marginTop: isMobile ? '10px' : 0 }">
<a-custom-statistic title="IPv6" :value="status.publicIP.ipv6">
<template #prefix>
<a-icon type="global" />
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.connectionCount" }}' hoverable>
<a-row>
<a-col :span="12">
<a-custom-statistic title="TCP" :value="status.tcpCount">
<template #prefix>
<a-icon type="swap" />
</template>
</a-custom-statistic>
</a-col>
<a-col :span="12">
<a-custom-statistic title="UDP" :value="status.udpCount">
<template #prefix>
<a-icon type="swap" />
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
</template>
</transition>
</a-spin>
</a-layout-content>
</a-layout>
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' :closable="true"
@ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
<a-alert type="warning" style="margin-bottom: 12px; width: fit-content"
<a-alert type="warning" style="margin-bottom: 12px; width: 100%;"
message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert>
<template v-for="version, index in versionModal.versions">
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'" style="margin-right: 12px; margin-bottom: 12px"
@click="switchV2rayVersion(version)">
[[ version ]]
</a-tag>
</template>
<a-list class="ant-xray-version-list" bordered style="width: 100%;">
<a-list-item class="ant-xray-version-list-item" v-for="version, index in versionModal.versions">
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ version ]]</a-tag>
<a-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`" @click="switchV2rayVersion(version)"></a-radio>
</a-list-item>
</a-list>
</a-modal>
<a-modal id="log-modal" v-model="logModal.visible"
:closable="true" @cancel="() => logModal.visible = false"
@@ -314,32 +392,39 @@
</a-form>
<div class="ant-input" style="height: auto; max-height: 500px; overflow: auto; margin-top: 0.5rem;" v-html="logModal.formattedLogs"></div>
</a-modal>
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
:closable="true" footer=""
<a-modal id="backup-modal"
v-model="backupModal.visible"
title='{{ i18n "pages.index.backupTitle" }}'
:closable="true"
footer=""
:class="themeSwitcher.currentTheme">
<a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
:message="backupModal.description"
show-icon>
</a-alert>
<a-space direction="horizontal" style="text-align: center; margin-bottom: 10px;">
<a-button type="primary" @click="exportDatabase()">
[[ backupModal.exportText ]]
</a-button>
<a-button type="primary" @click="importDatabase()">
[[ backupModal.importText ]]
</a-button>
</a-space>
<a-list class="ant-backup-list" bordered style="width: 100%;">
<a-list-item class="ant-backup-list-item">
<a-list-item-meta>
<template #title>{{ i18n "pages.index.exportDatabase" }}</template>
<template #description>{{ i18n "pages.index.exportDatabaseDesc" }}</template>
</a-list-item-meta>
<a-button @click="exportDatabase()" type="primary" icon="download"/>
</a-list-item>
<a-list-item class="ant-backup-list-item">
<a-list-item-meta>
<template #title>{{ i18n "pages.index.importDatabase" }}</template>
<template #description>{{ i18n "pages.index.importDatabaseDesc" }}</template>
</a-list-item-meta>
<a-button @click="importDatabase()" type="primary" icon="upload" />
</a-list-item>
</a-list>
</a-modal>
</a-layout>
{{template "js" .}}
<script src="{{ .base_path }}assets/clipboard/clipboard.min.js?{{ .cur_ver }}"></script>
{{template "component/themeSwitcher" .}}
{{template "component/aThemeSwitch" .}}
{{template "component/aCustomStatistic" .}}
{{template "textModal"}}
<script>
const State = {
Running: "running",
Stop: "stop",
Error: "error",
Running: "running",
Stop: "stop",
Error: "error",
}
Object.freeze(State);
@@ -354,7 +439,7 @@
if (this.total === 0) {
return 0;
}
return toFixed(this.current / this.total * 100, 2);
return NumberFormatter.toFixed(this.current / this.total * 100, 2);
}
get color() {
@@ -370,7 +455,7 @@
}
class Status {
constructor(data) {
constructor(data, isLoaded = false) {
this.cpu = new CurTotal(0, 0);
this.cpuCores = 0;
this.logicalPro = 0;
@@ -390,14 +475,16 @@
this.xray = { state: State.Stop, errorMsg: "", version: "", color: "" };
if (data == null) {
return;
return;
}
this.isLoaded = isLoaded;
this.cpu = new CurTotal(data.cpu, 100);
this.cpuCores = data.cpuCores;
this.logicalPro = data.logicalPro;
this.cpuSpeedMhz = data.cpuSpeedMhz;
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 => NumberFormatter.toFixed(load, 2));
this.mem = new CurTotal(data.mem.current, data.mem.total);
this.netIO = data.netIO;
this.netTraffic = data.netTraffic;
@@ -492,24 +579,11 @@
const backupModal = {
visible: false,
title: '',
description: '',
exportText: '',
importText: '',
show({
title = '{{ i18n "pages.index.backupTitle" }}',
description = '{{ i18n "pages.index.backupDescription" }}',
exportText = '{{ i18n "pages.index.exportDatabase" }}',
importText = '{{ i18n "pages.index.importDatabase" }}',
}) {
this.title = title;
this.description = description;
this.exportText = exportText;
this.importText = importText;
this.visible = true;
show() {
this.visible = true;
},
hide() {
this.visible = false;
this.visible = false;
},
};
@@ -526,6 +600,8 @@
spinning: false,
loadingTip: '{{ i18n "loading"}}',
showAlert: false,
showIp: false,
isMobile: window.innerWidth <= 768
},
methods: {
loading(spinning, tip = '{{ i18n "loading"}}') {
@@ -536,14 +612,14 @@
try {
const msg = await HttpUtil.post('/server/status');
if (msg.success) {
this.setStatus(msg.obj);
this.setStatus(msg.obj, true);
}
} catch (e) {
console.error("Failed to get status:", e);
}
},
setStatus(data) {
this.status = new Status(data);
setStatus(data, isLoaded = false) {
this.status = new Status(data, isLoaded);
},
async openSelectV2rayVersion() {
this.loading(true);
@@ -605,12 +681,7 @@
txtModal.show('config.json', JSON.stringify(msg.obj, null, 2), 'config.json');
},
openBackup() {
backupModal.show({
title: '{{ i18n "pages.index.backupTitle" }}',
description: '{{ i18n "pages.index.backupDescription" }}',
exportText: '{{ i18n "pages.index.exportDatabase" }}',
importText: '{{ i18n "pages.index.importDatabase" }}',
});
backupModal.show();
},
exportDatabase() {
window.location = basePath + 'server/getDb';

View File

@@ -108,16 +108,17 @@
</a-row>
</a-card>
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings"}}'>
<a-list item-layout="horizontal">
<a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.settings.remarkModel"}}'>
<template slot="description">{{ i18n "pages.settings.sampleRemark"}}: <i>#[[ remarkSample ]]</i></template>
</a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
<a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings" }}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.xray.generalConfigs"}}'>
<a-setting-list-item paddings="small">
<template #title>
{{ i18n "pages.settings.remarkModel"}}
</template>
<template #description>
{{ i18n "pages.settings.sampleRemark"}}: <i>#[[ remarkSample ]]</i>
</template>
<template #control>
<a-input-group style="width: 100%;">
<a-select style="padding-right: .5rem; min-width: 80%; width: auto;"
mode="multiple"
@@ -129,271 +130,530 @@
<a-select-option v-for="key in remarkSeparators" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-input-group>
</a-col>
</a-row>
</a-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningDomain"}}' desc='{{ i18n "pages.settings.panelListeningDomainDesc"}}' v-model="allSetting.webDomain"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="1" :max="65531"></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.panelUrlPath"}}' desc='{{ i18n "pages.settings.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.sessionMaxAge" }}' desc='{{ i18n "pages.settings.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="60"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.pageSize" }}' desc='{{ i18n "pages.settings.pageSizeDesc" }}' v-model="allSetting.pageSize" :min="0" :step="5"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.expireTimeDiff" }}' desc='{{ i18n "pages.settings.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.trafficDiff" }}' desc='{{ i18n "pages.settings.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.timeZone"}}' desc='{{ i18n "pages.settings.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
<a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.settings.datepicker"}}'>
<template slot="description">{{ i18n "pages.settings.datepickerDescription"}}</template>
</a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme" v-model="datepicker">
<a-select-option v-for="item in datepickerList" :value="item.value">
<span v-text="item.name"></span>
</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
<a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title="Language"></a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select ref="selectLang"
v-model="lang"
@change="setLang(lang)"
:dropdown-class-name="themeSwitcher.currentTheme"
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>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.panelListeningIP"}}</template>
<template #description>{{ i18n "pages.settings.panelListeningIPDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.webListen"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.panelListeningDomain"}}</template>
<template #description>{{ i18n "pages.settings.panelListeningDomainDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.webDomain"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.panelPort"}}</template>
<template #description>{{ i18n "pages.settings.panelPortDesc"}}</template>
<template #control>
<a-input-number :min="1" :min="65531" v-model="allSetting.webPort" style="width: 100%;"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.panelUrlPath"}}</template>
<template #description>{{ i18n "pages.settings.panelUrlPathDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.webBasePath"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.sessionMaxAge" }}</template>
<template #description>{{ i18n "pages.settings.sessionMaxAgeDesc" }}</template>
<template #control>
<a-input-number :min="60" v-model="allSetting.sessionMaxAge" style="width: 100%;"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.pageSize" }}</template>
<template #description>{{ i18n "pages.settings.pageSizeDesc" }}</template>
<template #control>
<a-input-number :min="0" step="5" v-model="allSetting.pageSize" style="width: 100%;"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.language"}}</template>
<template #control>
<a-select ref="selectLang"
v-model="lang"
@change="LanguageManager.setLanguage(lang)"
:dropdown-class-name="themeSwitcher.currentTheme"
style="width: 100%">
<a-select-option :value="l.value" :label="l.value" v-for="l in LanguageManager.supportedLanguages">
<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-setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.notifications" }}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.expireTimeDiff" }}</template>
<template #description>{{ i18n "pages.settings.expireTimeDiffDesc" }}</template>
<template #control>
<a-input-number :min="0" v-model="allSetting.expireDiff" style="width: 100%;"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.trafficDiff" }}</template>
<template #description>{{ i18n "pages.settings.trafficDiffDesc" }}</template>
<template #control>
<a-input-number :min="0" v-model="allSetting.trafficDiff" style="width: 100%;"></a-input>
</template>
</a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.certs" }}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.publicKeyPath"}}</template>
<template #description>{{ i18n "pages.settings.publicKeyPathDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.webCertFile"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.privateKeyPath"}}</template>
<template #description>{{ i18n "pages.settings.privateKeyPathDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.webKeyFile"></a-input>
</template>
</a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.externalTraffic" }}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.externalTrafficInformEnable"}}</template>
<template #description>{{ i18n "pages.settings.externalTrafficInformEnableDesc"}}</template>
<template #control>
<a-switch v-model="allSetting.externalTrafficInformEnable"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.externalTrafficInformURI"}}</template>
<template #description>{{ i18n "pages.settings.externalTrafficInformURIDesc"}}</template>
<template #control>
<a-input type="text" placeholder="(http|https)://domain[:port]/path/" v-model="allSetting.externalTrafficInformURI"></a-input>
</template>
</a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.dateAndTime" }}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.timeZone"}}</template>
<template #description>{{ i18n "pages.settings.timeZoneDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.timeLocation"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.datepicker"}}</template>
<template #description>{{ i18n "pages.settings.datepickerDescription"}}</template>
<template #control>
<a-select style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme" v-model="datepicker">
<a-select-option v-for="item in datepickerList" :value="item.value">
<span v-text="item.name"></span>
</a-select-option>
</a-select>
</template>
</a-setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings"}}' style="padding: 20px;">
<a-divider>{{ i18n "pages.settings.security.admin"}}</a-divider>
<a-form layout="horizontal" :colon="false" style="float: left; margin-bottom: 2rem;" :label-col="{ md: {span:10} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.settings.oldUsername"}}'>
<a-input autocomplete="username" v-model="user.oldUsername"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.settings.currentPassword"}}'>
<password-input autocomplete="current-password" v-model="user.oldPassword"></password-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.settings.newUsername"}}'>
<a-input v-model="user.newUsername"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.settings.newPassword"}}'>
<password-input autocomplete="new-password" v-model="user.newPassword"></password-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
</a-form-item>
</a-form>
<a-divider>{{ i18n "pages.settings.security.secret"}}</a-divider>
<a-form style="padding: 0 20px;">
<a-list-item>
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.settings.security.loginSecurity" }}'
description='{{ i18n "pages.settings.security.loginSecurityDesc" }}'>
</a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-switch @change="toggleToken(allSetting.secretEnable)" v-model="allSetting.secretEnable"></a-switch>
<a-icon style="margin-left: 1rem;" v-if="allSetting.secretEnable" :spin="this.changeSecret" type="sync" @click="getNewSecret"></a-icon>
</template>
</a-col>
</a-row>
</a-list-item>
<a-list-item>
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.settings.security.secretToken" }}'
description='{{ i18n "pages.settings.security.secretTokenDesc" }}'>
</a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-textarea type="text" :disabled="!allSetting.secretEnable" v-model="user.loginSecret"></a-textarea>
</template>
</a-col>
</a-row>
</a-list-item>
<a-button type="primary" :loading="this.changeSecret" @click="updateSecret">{{ i18n "confirm" }}</a-button>
</a-form>
<a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings" }}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.security.admin"}}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.oldUsername"}}</template>
<template #control>
<a-input autocomplete="username" v-model="user.oldUsername"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.currentPassword"}}</template>
<template #control>
<a-password-input autocomplete="current-password" v-model="user.oldPassword"></a-password-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.newUsername"}}</template>
<template #control>
<a-input v-model="user.newUsername"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.newPassword"}}</template>
<template #control>
<a-password-input autocomplete="new-password" v-model="user.newPassword"></a-password-input>
</template>
</a-setting-list-item>
<a-list-item>
<a-space direction="horizontal" style="padding: 0 20px;">
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
</a-space>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.security.secret"}}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.security.loginSecurity" }}</template>
<template #description>{{ i18n "pages.settings.security.loginSecurityDesc" }}</template>
<template #control>
<a-switch @change="toggleToken(allSetting.secretEnable)" v-model="allSetting.secretEnable"></a-switch>
<a-icon style="margin-left: 1rem;" v-if="allSetting.secretEnable" :spin="this.changeSecret" type="sync" @click="getNewSecret"></a-icon>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.security.secretToken" }}</template>
<template #description>{{ i18n "pages.settings.security.secretTokenDesc" }}</template>
<template #control>
<a-textarea type="text" :disabled="!allSetting.secretEnable" v-model="user.loginSecret"></a-textarea>
</template>
</a-setting-list-item>
<a-list-item>
<a-space direction="horizontal" style="padding: 0 20px;">
<a-button type="primary" :loading="this.changeSecret" @click="updateSecret">{{ i18n "confirm" }}</a-button>
</a-space>
</a-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
<a-list item-layout="horizontal">
<setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramToken"}}' desc='{{ i18n "pages.settings.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></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="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="text" title='{{ i18n "pages.settings.telegramProxy"}}' desc='{{ i18n "pages.settings.telegramProxyDesc"}}' v-model="allSetting.tgBotProxy" placeholder="socks5://user:pass@host:port"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramAPIServer"}}' desc='{{ i18n "pages.settings.telegramAPIServerDesc"}}' v-model="allSetting.tgBotAPIServer" placeholder="https://api.example.com"></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.currentTheme" 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-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings" }}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.xray.generalConfigs"}}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.telegramBotEnable" }}</template>
<template #description>{{ i18n "pages.settings.telegramBotEnableDesc" }}</template>
<template #control>
<a-switch v-model="allSetting.tgBotEnable"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.telegramToken"}}</template>
<template #description>{{ i18n "pages.settings.telegramTokenDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.tgBotToken"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.telegramChatId"}}</template>
<template #description>{{ i18n "pages.settings.telegramChatIdDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.tgBotChatId"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.telegramBotLanguage"}}</template>
<template #control>
<a-select ref="selectBotLang" v-model="allSetting.tgLang" :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%">
<a-select-option :value="l.value" :label="l.value" v-for="l in LanguageManager.supportedLanguages">
<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-setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.notifications" }}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.telegramNotifyTime"}}</template>
<template #description>{{ i18n "pages.settings.telegramNotifyTimeDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.tgRunTime"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.tgNotifyBackup" }}</template>
<template #description>{{ i18n "pages.settings.tgNotifyBackupDesc" }}</template>
<template #control>
<a-switch v-model="allSetting.tgBotBackup"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.tgNotifyLogin" }}</template>
<template #description>{{ i18n "pages.settings.tgNotifyLoginDesc" }}</template>
<template #control>
<a-switch v-model="allSetting.tgBotLoginNotify"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.tgNotifyCpu" }}</template>
<template #description>{{ i18n "pages.settings.tgNotifyCpuDesc" }}</template>
<template #control>
<a-input-number :min="0" :min="100" v-model="allSetting.tgCpu" style="width: 100%;"></a-switch>
</template>
</a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.proxyAndServer" }}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.telegramProxy"}}</template>
<template #description>{{ i18n "pages.settings.telegramProxyDesc"}}</template>
<template #control>
<a-input type="text" placeholder="socks5://user:pass@host:port" v-model="allSetting.tgBotProxy"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.telegramAPIServer"}}</template>
<template #description>{{ i18n "pages.settings.telegramAPIServerDesc"}}</template>
<template #control>
<a-input type="text" placeholder="https://api.example.com" v-model="allSetting.tgBotAPIServer"></a-input>
</template>
</a-setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}'>
<a-list item-layout="horizontal">
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort" :min="1" :max="65531"></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.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.subURI"}}' desc='{{ i18n "pages.settings.subURIDesc"}}' v-model="allSetting.subURI" placeholder="(http|https)://domain[:port]/path/"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates" :min="1"></setting-list-item>
</a-list>
<a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.xray.generalConfigs"}}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subEnable"}}</template>
<template #description>{{ i18n "pages.settings.subEnableDesc"}}</template>
<template #control>
<a-switch v-model="allSetting.subEnable"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subTitle"}}</template>
<template #description>{{ i18n "pages.settings.subTitleDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.subTitle"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subListen"}}</template>
<template #description>{{ i18n "pages.settings.subListenDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.subListen"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subDomain"}}</template>
<template #description>{{ i18n "pages.settings.subDomainDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.subDomain"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subPort"}}</template>
<template #description>{{ i18n "pages.settings.subPortDesc"}}</template>
<template #control>
<a-input-number v-model="allSetting.subPort" :min="1" :min="65531" style="width: 100%;"></a-input-number>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subPath"}}</template>
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.subPath"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subURI"}}</template>
<template #description>{{ i18n "pages.settings.subURIDesc"}}</template>
<template #control>
<a-input type="text" placeholder="(http|https)://domain[:port]/path/" v-model="allSetting.subURI"></a-input>
</template>
</a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.information" }}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subEncrypt"}}</template>
<template #description>{{ i18n "pages.settings.subEncryptDesc"}}</template>
<template #control>
<a-switch v-model="allSetting.subEncrypt"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subShowInfo"}}</template>
<template #description>{{ i18n "pages.settings.subShowInfoDesc"}}</template>
<template #control>
<a-switch v-model="allSetting.subShowInfo"></a-switch>
</template>
</a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.certs" }}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subCertPath"}}</template>
<template #description>{{ i18n "pages.settings.subCertPathDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.subCertFile"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subKeyPath"}}</template>
<template #description>{{ i18n "pages.settings.subKeyPathDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.subKeyFile"></a-input>
</template>
</a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.intervals"}}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subUpdates"}}</template>
<template #description>{{ i18n "pages.settings.subUpdatesDesc"}}</template>
<template #control>
<a-input-number :min="1" v-model="allSetting.subUpdates" style="width: 100%;"></a-input-number>
</template>
</a-setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }} Json' v-if="allSetting.subEnable">
<a-list item-layout="horizontal">
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subJsonPath"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subURI"}}' desc='{{ i18n "pages.settings.subURIDesc"}}' v-model="allSetting.subJsonURI" placeholder="(http|https)://domain[:port]/path/"></setting-list-item>
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.settings.fragment"}}'>
<template slot="description">{{ i18n "pages.settings.fragmentDesc"}}</template>
</a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
<a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }} Json' v-if="allSetting.subEnable" style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.xray.generalConfigs"}}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subPath"}}</template>
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.subJsonPath"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subURI"}}</template>
<template #description>{{ i18n "pages.settings.subURIDesc"}}</template>
<template #control>
<a-input type="text" placeholder="(http|https)://domain[:port]/path/" v-model="allSetting.subJsonURI"></a-input>
</template>
</a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.fragment"}}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.fragment"}}</template>
<template #description>{{ i18n "pages.settings.fragmentDesc"}}</template>
<template #control>
<a-switch v-model="fragment"></a-switch>
</a-col>
</a-row>
<a-collapse v-if="fragment" style="margin-top: 14px;">
<a-collapse-panel header='{{ i18n "pages.settings.fragmentSett"}}' v-if="fragment">
<setting-list-item style="padding: 10px 20px" type="text" title='Packets' v-model="fragmentPackets" placeholder="1-1 | 1-3 | tlshello | ..."></setting-list-item>
<setting-list-item style="padding: 10px 20px" type="text" title='Length' v-model="fragmentLength" placeholder="100-200"></setting-list-item>
<setting-list-item style="padding: 10px 20px" type="text" title='Interval' v-model="fragmentInterval" placeholder="10-20"></setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-list-item>
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='Noises'>
<template slot="description">{{ i18n "pages.settings.noisesDesc"}}</template>
</a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
</template>
</a-setting-list-item>
<a-list-item v-if="fragment" style="padding: 10px 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.fragmentSett"}}' v-if="fragment">
<a-setting-list-item paddings="small">
<template #title>Packets</template>
<template #control>
<a-input type="text" v-model="fragmentPackets" placeholder="1-1 | 1-3 | tlshello | ..."></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Length</template>
<template #control>
<a-input type="text" v-model="fragmentLength" placeholder="100-200"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Interval</template>
<template #control>
<a-input type="text" v-model="fragmentInterval" placeholder="10-20"></a-input>
</template>
</a-setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header="Noises">
<a-setting-list-item paddings="small">
<template #title>Noises</template>
<template #description>{{ i18n "pages.settings.noisesDesc"}}</template>
<template #control>
<a-switch v-model="noises"></a-switch>
</a-col>
</a-row>
<a-collapse v-if="noises" style="margin-top: 14px;">
<a-collapse-panel v-for="(noise, index) in noisesArray" :key="index" :header="`Noise ${index + 1}`">
<a-list-item style="padding: 10px 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='Type'></a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
</template>
</a-setting-list-item>
<a-list-item v-if="noises" style="padding: 10px 20px;">
<a-collapse>
<a-collapse-panel v-for="(noise, index) in noisesArray" :key="index" :header="`Noise №${index + 1}`">
<a-setting-list-item paddings="small">
<template #title>Type</template>
<template #control>
<a-select :value="noise.type" style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme"
@change="(value) => updateNoiseType(index, value)">
<a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str']" :key="p">
<a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str', 'hex']" :key="p">
[[ p ]] </a-select-option>
</a-select>
</a-col>
</a-row>
</a-list-item>
<setting-list-item style="padding: 10px 20px" type="text" title='Packet' :value="noise.packet"
@input="(value) => updateNoisePacket(index, value)" placeholder="5-10"></setting-list-item>
<setting-list-item style="padding: 10px 20px" type="text" title='Delay (ms)' :value="noise.delay"
@input="(value) => updateNoiseDelay(index, value)" placeholder="10-20"></setting-list-item>
<a-button v-if="noisesArray.length > 1" type="danger" @click="removeNoise(index)">Remove</a-button>
</a-collapse-panel>
</a-collapse>
<a-button v-if="noises" type="primary" @click="addNoise" style="margin-top: 10px">Add Noise</a-button>
</a-list-item>
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.settings.mux"}}'>
<template slot="description">{{ i18n "pages.settings.muxDesc"}}</template>
</a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Packet</template>
<template #control>
<a-input type="text" :value="noise.packet" @input="(value) => updateNoisePacket(index, event.target.value)" placeholder="5-10"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Delay (ms)</template>
<template #control>
<a-input type="text" :value="noise.delay" @input="(value) => updateNoiseDelay(index, event.target.value)" placeholder="10-20"></a-input>
</template>
</a-setting-list-item>
<a-space direction="horizontal" style="padding: 10px 20px;">
<a-button v-if="noisesArray.length > 1" type="danger" @click="removeNoise(index)">Remove</a-button>
</a-space>
</a-collapse-panel>
</a-collapse>
<a-button v-if="noises" type="primary" @click="addNoise" style="margin-top: 10px">Add Noise</a-button>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.mux"}}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.mux"}}</template>
<template #description>{{ i18n "pages.settings.muxDesc"}}</template>
<template #control>
<a-switch v-model="enableMux"></a-switch>
</a-col>
</a-row>
<a-collapse v-if="enableMux" style="margin-top: 14px;">
<a-collapse-panel header='{{ i18n "pages.settings.muxSett"}}'>
<setting-list-item style="padding: 10px 20px" type="number" title='Concurrency' v-model="muxConcurrency" :min="-1" :max="1024"></setting-list-item>
<setting-list-item style="padding: 10px 20px" type="number" title='xudp Concurrency' v-model="muxXudpConcurrency" :min="-1" :max="1024"></setting-list-item>
<a-list-item style="padding: 10px 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='xudp UDP 443'></a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
</template>
</a-setting-list-item>
<a-list-item v-if="enableMux" style="padding: 10px 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.muxSett"}}'>
<a-setting-list-item paddings="small">
<template #title>Concurrency</template>
<template #control>
<a-input-number v-model="muxConcurrency" :min="-1" :max="1024" style="width: 100%;"></a-input-number>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>xudp Concurrency</template>
<template #control>
<a-input-number v-model="muxXudpConcurrency" :min="-1" :max="1024" style="width: 100%;"></a-input-number>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>xudp UDP 443</template>
<template #control>
<a-select v-model="muxXudpProxyUDP443" style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p" :label="p" v-for="p in ['reject', 'allow', 'skip']"> [[ p ]] </a-select-option>
</a-select>
</a-col>
</a-row>
</a-list-item>
</a-collapse-panel>
</a-collapse>
</a-list-item>
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.settings.direct"}}'>
<template slot="description">{{ i18n "pages.settings.directDesc"}}</template>
</a-list-item-meta>
</a-col>
<a-col :lg="24" :xl="12">
</template>
</a-setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.direct" }}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.direct"}}</template>
<template #description>{{ i18n "pages.settings.directDesc"}}</template>
<template #control>
<a-switch v-model="enableDirect"></a-switch>
</a-col>
</a-row>
<a-collapse v-if="enableDirect" style="margin-top: 14px;">
<a-collapse-panel header='{{ i18n "pages.xray.directips"}}'>
<a-list-item style="padding: 10px 20px">
<a-checkbox-group v-model="directIPs" :options="IPsOptions"></a-checkbox-group>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.directdomains"}}'>
<a-list-item style="padding: 10px 20px">
<a-checkbox-group v-model="directDomains" :options="DomainsOptions"></a-checkbox-group>
</a-list-item>
</a-collapse-panel>
</a-collapse>
</a-list-item>
</a-list>
</template>
</a-setting-list-item>
<a-list-item v-if="enableDirect" style="padding: 10px 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.direct"}}'>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.directips" }}</template>
<template #control>
<a-select mode="tags" style="width: 100%" v-model="directIPs" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" v-for="p in directIPsOptions">[[ p.label ]]</a-select-option>
</a-select>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.directdomains" }}</template>
<template #control>
<a-select mode="tags" style="width: 100%" v-model="directDomains" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label" v-for="p in diretDomainsOptions">[[ p.label ]]</a-select-option>
</a-select>
</template>
</a-setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
</a-tabs>
</a-space>
@@ -403,9 +663,9 @@
</a-layout>
{{template "js" .}}
<script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script>
{{template "component/themeSwitcher" .}}
{{template "component/password" .}}
{{template "component/setting"}}
{{template "component/aThemeSwitch" .}}
{{template "component/aPasswordInput" .}}
{{template "component/aSettingListItem" .}}
<script>
const app = new Vue({
delimiters: ['[[', ']]'],
@@ -419,7 +679,7 @@
allSetting: new AllSetting(),
saveBtnDisable: true,
user: {},
lang: getLang(),
lang: LanguageManager.getLanguage(),
remarkModels: { i: 'Inbound', e: 'Email', o: 'Other' },
remarkSeparators: [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'],
datepickerList: [{ name: 'Gregorian (Standard)', value: 'gregorian' }, { name: 'Jalalian (شمسی)', value: 'jalalian' }],
@@ -439,7 +699,7 @@
sockopt: {
tcpKeepAliveIdle: 100,
tcpMptcp: true,
tcpNoDelay: true
penetrate: true
}
}
},
@@ -476,25 +736,26 @@
]
},
],
IPsOptions: [
{ label: 'Private IP', value: 'private' },
{ label: '🇮🇷 Iran', value: 'ir' },
{ label: '🇨🇳 China', value: 'cn' },
{ label: '🇷🇺 Russia', value: 'ru' },
{ label: '🇻🇳 Vietnam', value: 'vn' },
{ label: '🇪🇸 Spain', value: 'es' },
{ label: '🇮🇩 Indonesia', value: 'id' },
{ label: '🇺🇦 Ukraine', value: 'ua' },
{ label: '🇹🇷 Türkiye', value: 'tr' },
{ label: '🇧🇷 Brazil', value: 'br' },
directIPsOptions: [
{ label: 'Private IP', value: 'geoip:private' },
{ label: '🇮🇷 Iran', value: 'geoip:ir' },
{ label: '🇨🇳 China', value: 'geoip:cn' },
{ label: '🇷🇺 Russia', value: 'geoip:ru' },
{ label: '🇻🇳 Vietnam', value: 'geoip:vn' },
{ label: '🇪🇸 Spain', value: 'geoip:es' },
{ label: '🇮🇩 Indonesia', value: 'geoip:id' },
{ label: '🇺🇦 Ukraine', value: 'geoip:ua' },
{ label: '🇹🇷 Türkiye', value: 'geoip:tr' },
{ label: '🇧🇷 Brazil', value: 'geoip:br' },
],
DomainsOptions: [
{ label: '🇮🇷 Iran', value: 'ir' },
{ label: '🇨🇳 China', value: 'cn' },
{ label: '🇷🇺 Russia', value: 'ru' },
{ label: 'Apple', value: 'apple' },
{ label: 'Meta', value: 'meta' },
{ label: 'Google', value: 'google' },
diretDomainsOptions: [
{ label: 'Private DNS', value: 'geosite:private' },
{ label: '🇮🇷 Iran', value: 'geosite:category-ir' },
{ label: '🇨🇳 China', value: 'geosite:cn' },
{ label: '🇷🇺 Russia', value: 'geosite:category-ru' },
{ label: 'Apple', value: 'geosite:apple' },
{ label: 'Meta', value: 'geosite:meta' },
{ label: 'Google', value: 'geosite:google' },
],
get remarkModel() {
rm = this.allSetting.remarkModel;
@@ -578,7 +839,7 @@
if (host == this.oldAllSetting.webDomain) host = null;
if (port == this.oldAllSetting.webPort) port = null;
const isTLS = webCertFile !== "" || webKeyFile !== "";
const url = buildURL({ host, port, isTLS, base, path: "panel/settings" });
const url = URLBuilder.buildURL({ host, port, isTLS, base, path: "panel/settings" });
window.location.replace(url);
}
},
@@ -752,7 +1013,7 @@
const rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return [];
const ipRule = rules.find(r => r.ip);
return ipRule?.ip.map(d => d.replace("geoip:", "")) ?? [];
return ipRule?.ip ?? [];
},
set: function (v) {
let rules = JSON.parse(this.allSetting.subJsonRules);
@@ -766,7 +1027,7 @@
rules[ruleIndex].ip = [];
v.forEach(d => {
rules[ruleIndex].ip.push("geoip:" + d);
rules[ruleIndex].ip.push(d);
});
}
this.allSetting.subJsonRules = JSON.stringify(rules);
@@ -778,34 +1039,18 @@
const rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return [];
const domainRule = rules.find(r => r.domain);
return domainRule?.domain.map(d => {
if (d.startsWith("geosite:category-")) {
return d.replace("geosite:category-", "");
}
return d.replace("geosite:", "");
})
?? [];
return domainRule?.domain ?? [];
},
set: function (v) {
let rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return;
if (v.length == 0) {
rules = rules.filter(r => !r.domain);
} else {
let ruleIndex = rules.findIndex(r => r.domain);
if (ruleIndex == -1) ruleIndex = rules.push(this.defaultRules[0]) - 1;
rules[ruleIndex].domain = [];
v.forEach(d => {
let category = '';
if (["cn", "apple", "meta", "google"].includes(d)) {
category = "";
} else if (["ru", "ir"].includes(d)) {
category = "category-";
}
rules[ruleIndex].domain.push("geosite:" + category + d);
});
rules[ruleIndex].domain = v;
}
this.allSetting.subJsonRules = JSON.stringify(rules);
}
@@ -838,4 +1083,4 @@
});
</script>
</body>
</html>
</html>

View File

@@ -65,15 +65,15 @@
</tr>
<tr>
<td>WARP+ Data</td>
<td>[[ sizeFormat(warpModal.warpConfig.account.premium_data) ]]</td>
<td>[[ SizeFormatter.sizeFormat(warpModal.warpConfig.account.premium_data) ]]</td>
</tr>
<tr class="client-table-odd-row">
<td>Quota</td>
<td>[[ sizeFormat(warpModal.warpConfig.account.quota) ]]</td>
<td>[[ SizeFormatter.sizeFormat(warpModal.warpConfig.account.quota) ]]</td>
</tr>
<tr v-if="!ObjectUtil.isEmpty(warpModal.warpConfig.account.usage)">
<td>Usage</td>
<td>[[ sizeFormat(warpModal.warpConfig.account.usage) ]]</td>
<td>[[ SizeFormatter.sizeFormat(warpModal.warpConfig.account.usage) ]]</td>
</tr>
</template>
</table>
@@ -176,10 +176,10 @@
},
async register() {
warpModal.loading(true);
keys = Wireguard.generateKeypair();
const keys = Wireguard.generateKeypair();
const msg = await HttpUtil.post('/panel/xray/warp/reg', keys);
if (msg.success) {
resp = JSON.parse(msg.obj);
const resp = JSON.parse(msg.obj);
warpModal.warpData = resp.data;
warpModal.warpConfig = resp.config;
this.collectConfig();

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@
</a-form-item>
<a-form-item label='Protocol'>
<a-select v-model="ruleModal.rule.protocol" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in ['http','tls','bittorrent']" :value="x">[[ x ]]</a-select-option>
<a-select-option v-for="x in ['http','tls','bittorrent','quic']" :value="x">[[ x ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Attributes'>

View File

@@ -11,6 +11,7 @@ import (
"sort"
"time"
"slices"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
@@ -106,7 +107,7 @@ func (j *CheckClientIpJob) hasLimitIp() bool {
func (j *CheckClientIpJob) processLogFile() bool {
ipRegex := regexp.MustCompile(`from \[?([0-9a-fA-F:.]+)\]?:\d+ accepted`)
ipRegex := regexp.MustCompile(`from (?:tcp:|udp:)?\[?([0-9a-fA-F\.:]+)\]?:\d+ accepted`)
emailRegex := regexp.MustCompile(`email: (.+)$`)
accessLogPath, _ := xray.GetAccessLogPath()
@@ -193,13 +194,7 @@ func (j *CheckClientIpJob) checkError(e error) {
}
func (j *CheckClientIpJob) contains(s []string, str string) bool {
for _, v := range s {
if v == str {
return true
}
}
return false
return slices.Contains(s, str)
}
func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {

View File

@@ -23,7 +23,7 @@ func (j *CheckCpuJob) Run() {
threshold, _ := j.settingService.GetTgCpu()
// get latest status of server
percent, err := cpu.Percent(1*time.Second, false)
percent, err := cpu.Percent(1*time.Minute, false)
if err == nil && percent[0] > float64(threshold) {
msg := j.tgbotService.I18nBot("tgbot.messages.cpuThreshold",
"Percent=="+strconv.FormatFloat(percent[0], 'f', 2, 64),

View File

@@ -1,11 +1,16 @@
package job
import (
"encoding/json"
"x-ui/logger"
"x-ui/web/service"
"x-ui/xray"
"github.com/valyala/fasthttp"
)
type XrayTrafficJob struct {
settingService service.SettingService
xrayService service.XrayService
inboundService service.InboundService
outboundService service.OutboundService
@@ -31,7 +36,36 @@ func (j *XrayTrafficJob) Run() {
if err != nil {
logger.Warning("add outbound traffic failed:", err)
}
if ExternalTrafficInformEnable, err := j.settingService.GetExternalTrafficInformEnable(); ExternalTrafficInformEnable {
j.informTrafficToExternalAPI(traffics, clientTraffics)
} else if err != nil {
logger.Warning("get ExternalTrafficInformEnable failed:", err)
}
if needRestart0 || needRestart1 {
j.xrayService.SetToNeedRestart()
}
}
func (j *XrayTrafficJob) informTrafficToExternalAPI(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) {
informURL, err := j.settingService.GetExternalTrafficInformURI()
if err != nil {
logger.Warning("get ExternalTrafficInformURI failed:", err)
return
}
requestBody, err := json.Marshal(map[string]any{"clientTraffics": clientTraffics, "inboundTraffics": inboundTraffics})
if err != nil {
logger.Warning("parse client/inbound traffic failed:", err)
return
}
request := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(request)
request.Header.SetMethod("POST")
request.Header.SetContentType("application/json; charset=UTF-8")
request.SetBody([]byte(requestBody))
request.SetRequestURI(informURL)
response := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(response)
if err := fasthttp.Do(request, response); err != nil {
logger.Warning("POST ExternalTrafficInformURI failed:", err)
}
}

View File

@@ -48,13 +48,13 @@ func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
return nil
}
func createTemplateData(params []string, seperator ...string) map[string]interface{} {
func createTemplateData(params []string, seperator ...string) map[string]any {
var sep string = "=="
if len(seperator) > 0 {
sep = seperator[0]
}
templateData := make(map[string]interface{})
templateData := make(map[string]any)
for _, param := range params {
parts := strings.SplitN(param, sep, 2)
templateData[parts[0]] = parts[1]

View File

@@ -51,8 +51,8 @@
"system": {
"statsInboundDownlink": true,
"statsInboundUplink": true,
"statsOutboundDownlink": true,
"statsOutboundUplink": true
"statsOutboundDownlink": false,
"statsOutboundUplink": false
}
},
"routing": {

View File

@@ -413,13 +413,13 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
return false, err
}
var settings map[string]interface{}
var settings map[string]any
err = json.Unmarshal([]byte(data.Settings), &settings)
if err != nil {
return false, err
}
interfaceClients := settings["clients"].([]interface{})
interfaceClients := settings["clients"].([]any)
existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil {
return false, err
@@ -450,13 +450,13 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
}
}
var oldSettings map[string]interface{}
var oldSettings map[string]any
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil {
return false, err
}
oldClients := oldSettings["clients"].([]interface{})
oldClients := oldSettings["clients"].([]any)
oldClients = append(oldClients, interfaceClients...)
oldSettings["clients"] = oldClients
@@ -489,7 +489,7 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
if oldInbound.Protocol == "shadowsocks" {
cipher = oldSettings["method"].(string)
}
err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]interface{}{
err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]any{
"email": client.Email,
"id": client.ID,
"security": client.Security,
@@ -519,7 +519,7 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
logger.Error("Load Old Data Error")
return false, err
}
var settings map[string]interface{}
var settings map[string]any
err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
if err != nil {
return false, err
@@ -534,11 +534,11 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
client_key = "email"
}
interfaceClients := settings["clients"].([]interface{})
var newClients []interface{}
interfaceClients := settings["clients"].([]any)
var newClients []any
needApiDel := false
for _, client := range interfaceClients {
c := client.(map[string]interface{})
c := client.(map[string]any)
c_id := c[client_key].(string)
if c_id == clientId {
email, _ = c["email"].(string)
@@ -607,13 +607,13 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
return false, err
}
var settings map[string]interface{}
var settings map[string]any
err = json.Unmarshal([]byte(data.Settings), &settings)
if err != nil {
return false, err
}
interfaceClients := settings["clients"].([]interface{})
interfaceClients := settings["clients"].([]any)
oldInbound, err := s.GetInbound(data.Id)
if err != nil {
@@ -662,12 +662,12 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
}
}
var oldSettings map[string]interface{}
var oldSettings map[string]any
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil {
return false, err
}
settingsClients := oldSettings["clients"].([]interface{})
settingsClients := oldSettings["clients"].([]any)
settingsClients[clientIndex] = interfaceClients[0]
oldSettings["clients"] = settingsClients
@@ -732,7 +732,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
if oldInbound.Protocol == "shadowsocks" {
cipher = oldSettings["method"].(string)
}
err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]interface{}{
err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]any{
"email": clients[0].Email,
"id": clients[0].ID,
"security": clients[0].Security,
@@ -809,7 +809,7 @@ func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic
for _, traffic := range traffics {
if traffic.IsInbound {
err = tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
Updates(map[string]interface{}{
Updates(map[string]any{
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down),
}).Error
@@ -893,13 +893,13 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
return nil, err
}
for inbound_index := range inbounds {
settings := map[string]interface{}{}
settings := map[string]any{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{})
clients, ok := settings["clients"].([]any)
if ok {
var newClients []interface{}
var newClients []any
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
c := clients[client_index].(map[string]any)
for traffic_index := range dbClientTraffics {
if dbClientTraffics[traffic_index].ExpiryTime < 0 && c["email"] == dbClientTraffics[traffic_index].Email {
oldExpiryTime := c["expiryTime"].(float64)
@@ -909,7 +909,7 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
break
}
}
newClients = append(newClients, interface{}(c))
newClients = append(newClients, any(c))
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
@@ -951,7 +951,7 @@ func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
var clientsToAdd []struct {
protocol string
tag string
client map[string]interface{}
client map[string]any
}
for _, traffic := range traffics {
@@ -962,11 +962,11 @@ func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
return false, 0, err
}
for inbound_index := range inbounds {
settings := map[string]interface{}{}
settings := map[string]any{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients := settings["clients"].([]interface{})
clients := settings["clients"].([]any)
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
c := clients[client_index].(map[string]any)
for traffic_index, traffic := range traffics {
if traffic.Email == c["email"].(string) {
newExpiryTime := traffic.ExpiryTime
@@ -983,14 +983,14 @@ func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
struct {
protocol string
tag string
client map[string]interface{}
client map[string]any
}{
protocol: string(inbounds[inbound_index].Protocol),
tag: inbounds[inbound_index].Tag,
client: c,
})
}
clients[client_index] = interface{}(c)
clients[client_index] = any(c)
break
}
}
@@ -1147,7 +1147,7 @@ func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model
func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *model.Client) error {
result := tx.Model(xray.ClientTraffic{}).
Where("email = ?", email).
Updates(map[string]interface{}{
Updates(map[string]any{
"enable": true,
"email": client.Email,
"total": client.TotalGB,
@@ -1258,18 +1258,18 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (boo
return false, common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]interface{}
var settings map[string]any
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return false, err
}
clients := settings["clients"].([]interface{})
var newClients []interface{}
clients := settings["clients"].([]any)
var newClients []any
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["tgId"] = tgId
newClients = append(newClients, interface{}(c))
newClients = append(newClients, any(c))
}
}
settings["clients"] = newClients
@@ -1343,18 +1343,18 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
return false, false, common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]interface{}
var settings map[string]any
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return false, false, err
}
clients := settings["clients"].([]interface{})
var newClients []interface{}
clients := settings["clients"].([]any)
var newClients []any
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["enable"] = !clientOldEnabled
newClients = append(newClients, interface{}(c))
newClients = append(newClients, any(c))
}
}
settings["clients"] = newClients
@@ -1405,18 +1405,18 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
return false, common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]interface{}
var settings map[string]any
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return false, err
}
clients := settings["clients"].([]interface{})
var newClients []interface{}
clients := settings["clients"].([]any)
var newClients []any
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["limitIp"] = count
newClients = append(newClients, interface{}(c))
newClients = append(newClients, any(c))
}
}
settings["clients"] = newClients
@@ -1462,18 +1462,18 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
return false, common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]interface{}
var settings map[string]any
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return false, err
}
clients := settings["clients"].([]interface{})
var newClients []interface{}
clients := settings["clients"].([]any)
var newClients []any
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["expiryTime"] = expiry_time
newClients = append(newClients, interface{}(c))
newClients = append(newClients, any(c))
}
}
settings["clients"] = newClients
@@ -1522,18 +1522,18 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
return false, common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]interface{}
var settings map[string]any
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return false, err
}
clients := settings["clients"].([]interface{})
var newClients []interface{}
clients := settings["clients"].([]any)
var newClients []any
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["totalGB"] = totalGB * 1024 * 1024 * 1024
newClients = append(newClients, interface{}(c))
newClients = append(newClients, any(c))
}
}
settings["clients"] = newClients
@@ -1551,7 +1551,7 @@ func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
result := db.Model(xray.ClientTraffic{}).
Where("email = ?", clientEmail).
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
Updates(map[string]any{"enable": true, "up": 0, "down": 0})
err := result.Error
if err != nil {
@@ -1582,14 +1582,14 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, e
s.xrayApi.Init(p.GetAPIPort())
cipher := ""
if string(inbound.Protocol) == "shadowsocks" {
var oldSettings map[string]interface{}
var oldSettings map[string]any
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{}{
err1 := s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]any{
"email": client.Email,
"id": client.ID,
"security": client.Security,
@@ -1634,7 +1634,7 @@ func (s *InboundService) ResetAllClientTraffics(id int) error {
result := db.Model(xray.ClientTraffic{}).
Where(whereText, id).
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
Updates(map[string]any{"enable": true, "up": 0, "down": 0})
err := result.Error
return err
@@ -1645,7 +1645,7 @@ func (s *InboundService) ResetAllTraffics() error {
result := db.Model(model.Inbound{}).
Where("user_id > ?", 0).
Updates(map[string]interface{}{"up": 0, "down": 0})
Updates(map[string]any{"up": 0, "down": 0})
err := result.Error
return err
@@ -1681,17 +1681,17 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
if err != nil {
return err
}
var oldSettings map[string]interface{}
var oldSettings map[string]any
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil {
return err
}
oldClients := oldSettings["clients"].([]interface{})
var newClients []interface{}
oldClients := oldSettings["clients"].([]any)
var newClients []any
for _, client := range oldClients {
deplete := false
c := client.(map[string]interface{})
c := client.(map[string]any)
for _, email := range emails {
if email == c["email"].(string) {
deplete = true
@@ -1907,14 +1907,14 @@ func (s *InboundService) MigrationRequirements() {
return
}
for inbound_index := range inbounds {
settings := map[string]interface{}{}
settings := map[string]any{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{})
clients, ok := settings["clients"].([]any)
if ok {
// Fix Client configuration problems
var newClients []interface{}
var newClients []any
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
c := clients[client_index].(map[string]any)
// Add email='' if it is not exists
if _, ok := c["email"]; !ok {
@@ -1923,7 +1923,7 @@ func (s *InboundService) MigrationRequirements() {
// Convert string tgId to int64
if _, ok := c["tgId"]; ok {
var tgId interface{} = c["tgId"]
var tgId any = c["tgId"]
if tgIdStr, ok2 := tgId.(string); ok2 {
tgIdInt64, err := strconv.ParseInt(strings.ReplaceAll(tgIdStr, " ", ""), 10, 64)
if err == nil {
@@ -1938,7 +1938,7 @@ func (s *InboundService) MigrationRequirements() {
c["flow"] = ""
}
}
newClients = append(newClients, interface{}(c))
newClients = append(newClients, any(c))
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
@@ -1985,14 +1985,14 @@ func (s *InboundService) MigrationRequirements() {
}
for _, ep := range externalProxy {
var reverses interface{}
var stream map[string]interface{}
var reverses any
var stream map[string]any
json.Unmarshal(ep.StreamSettings, &stream)
if tlsSettings, ok := stream["tlsSettings"].(map[string]interface{}); ok {
if settings, ok := tlsSettings["settings"].(map[string]interface{}); ok {
if domains, ok := settings["domains"].([]interface{}); ok {
if tlsSettings, ok := stream["tlsSettings"].(map[string]any); ok {
if settings, ok := tlsSettings["settings"].(map[string]any); ok {
if domains, ok := settings["domains"].([]any); ok {
for _, domain := range domains {
if domainMap, ok := domain.(map[string]interface{}); ok {
if domainMap, ok := domain.(map[string]any); ok {
domainMap["forceTls"] = "same"
domainMap["port"] = ep.Port
domainMap["dest"] = domainMap["domain"].(string)

View File

@@ -89,7 +89,7 @@ func (s *OutboundService) ResetOutboundTraffic(tag string) error {
result := db.Model(model.OutboundTraffics{}).
Where(whereText, tag).
Updates(map[string]interface{}{"up": 0, "down": 0, "total": 0})
Updates(map[string]any{"up": 0, "down": 0, "total": 0})
err := result.Error
if err != nil {

View File

@@ -92,6 +92,8 @@ type Release struct {
type ServerService struct {
xrayService XrayService
inboundService InboundService
cachedIPv4 string
cachedIPv6 string
}
func getPublicIP(url string) string {
@@ -120,6 +122,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
T: now,
}
// CPU stats
percents, err := cpu.Percent(0, false)
if err != nil {
logger.Warning("get cpu percent failed:", err)
@@ -133,22 +136,17 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
}
status.LogicalPro = runtime.NumCPU()
if p != nil && p.IsRunning() {
status.AppStats.Uptime = p.GetUptime()
} else {
status.AppStats.Uptime = 0
}
cpuInfos, err := cpu.Info()
if err != nil {
logger.Warning("get cpu info failed:", err)
} else if len(cpuInfos) > 0 {
cpuInfo := cpuInfos[0]
status.CpuSpeedMhz = cpuInfo.Mhz // setting CPU speed in MHz
status.CpuSpeedMhz = cpuInfos[0].Mhz
} else {
logger.Warning("could not find cpu info")
}
// Uptime
upTime, err := host.Uptime()
if err != nil {
logger.Warning("get uptime failed:", err)
@@ -156,6 +154,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
status.Uptime = upTime
}
// Memory stats
memInfo, err := mem.VirtualMemory()
if err != nil {
logger.Warning("get virtual memory failed:", err)
@@ -172,14 +171,16 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
status.Swap.Total = swapInfo.Total
}
distInfo, err := disk.Usage("/")
// Disk stats
diskInfo, err := disk.Usage("/")
if err != nil {
logger.Warning("get dist usage failed:", err)
logger.Warning("get disk usage failed:", err)
} else {
status.Disk.Current = distInfo.Used
status.Disk.Total = distInfo.Total
status.Disk.Current = diskInfo.Used
status.Disk.Total = diskInfo.Total
}
// Load averages
avgState, err := load.Avg()
if err != nil {
logger.Warning("get load avg failed:", err)
@@ -187,6 +188,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
status.Loads = []float64{avgState.Load1, avgState.Load5, avgState.Load15}
}
// Network stats
ioStats, err := net.IOCounters(false)
if err != nil {
logger.Warning("get io counters failed:", err)
@@ -207,6 +209,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
logger.Warning("can not find io counters")
}
// TCP/UDP connections
status.TcpCount, err = sys.GetTCPCount()
if err != nil {
logger.Warning("get tcp connections failed:", err)
@@ -217,9 +220,15 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
logger.Warning("get udp connections failed:", err)
}
status.PublicIP.IPv4 = getPublicIP("https://api.ipify.org")
status.PublicIP.IPv6 = getPublicIP("https://api6.ipify.org")
// IP fetching with caching
if s.cachedIPv4 == "" || s.cachedIPv6 == "" {
s.cachedIPv4 = getPublicIP("https://api.ipify.org")
s.cachedIPv6 = getPublicIP("https://api6.ipify.org")
}
status.PublicIP.IPv4 = s.cachedIPv4
status.PublicIP.IPv6 = s.cachedIPv6
// Xray status
if s.xrayService.IsXrayRunning() {
status.Xray.State = Running
status.Xray.ErrorMsg = ""
@@ -233,9 +242,10 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
status.Xray.ErrorMsg = s.xrayService.GetXrayResult()
}
status.Xray.Version = s.xrayService.GetXrayVersion()
// Application stats
var rtm runtime.MemStats
runtime.ReadMemStats(&rtm)
status.AppStats.Mem = rtm.Sys
status.AppStats.Threads = uint32(runtime.NumGoroutine())
if p != nil && p.IsRunning() {
@@ -285,9 +295,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
continue
}
if (major == 1 && minor == 8 && patch == 24) ||
(major == 24 && ((minor > 11) || (minor == 11 && patch >= 30))) ||
(major > 24) {
if major > 25 || (major == 25 && minor > 3) || (major == 25 && minor == 3 && patch >= 3) {
versions = append(versions, release.TagName)
}
}
@@ -442,7 +450,7 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str
return lines
}
func (s *ServerService) GetConfigJson() (interface{}, error) {
func (s *ServerService) GetConfigJson() (any, error) {
config, err := s.xrayService.GetXrayConfig()
if err != nil {
return nil, err
@@ -452,7 +460,7 @@ func (s *ServerService) GetConfigJson() (interface{}, error) {
return nil, err
}
var jsonData interface{}
var jsonData any
err = json.Unmarshal(contents, &jsonData)
if err != nil {
return nil, err
@@ -583,7 +591,7 @@ func (s *ServerService) ImportDB(file multipart.File) error {
return nil
}
func (s *ServerService) GetNewX25519Cert() (interface{}, error) {
func (s *ServerService) GetNewX25519Cert() (any, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "x25519")
var out bytes.Buffer
@@ -601,7 +609,7 @@ func (s *ServerService) GetNewX25519Cert() (interface{}, error) {
privateKey := strings.TrimSpace(privateKeyLine[1])
publicKey := strings.TrimSpace(publicKeyLine[1])
keyPair := map[string]interface{}{
keyPair := map[string]any{
"privateKey": privateKey,
"publicKey": publicKey,
}

View File

@@ -24,56 +24,59 @@ import (
var xrayTemplateConfig string
var defaultValueMap = map[string]string{
"xrayTemplateConfig": xrayTemplateConfig,
"webListen": "",
"webDomain": "",
"webPort": "2053",
"webCertFile": "",
"webKeyFile": "",
"secret": random.Seq(32),
"webBasePath": "/",
"sessionMaxAge": "60",
"pageSize": "50",
"expireDiff": "0",
"trafficDiff": "0",
"remarkModel": "-ieo",
"timeLocation": "Asia/Tehran",
"tgBotEnable": "false",
"tgBotToken": "",
"tgBotProxy": "",
"tgBotAPIServer": "",
"tgBotChatId": "",
"tgRunTime": "@daily",
"tgBotBackup": "false",
"tgBotLoginNotify": "true",
"tgCpu": "80",
"tgLang": "en-US",
"secretEnable": "false",
"subEnable": "false",
"subListen": "",
"subPort": "2096",
"subPath": "/sub/",
"subDomain": "",
"subCertFile": "",
"subKeyFile": "",
"subUpdates": "12",
"subEncrypt": "true",
"subShowInfo": "true",
"subURI": "",
"subJsonPath": "/json/",
"subJsonURI": "",
"subJsonFragment": "",
"subJsonNoises": "",
"subJsonMux": "",
"subJsonRules": "",
"datepicker": "gregorian",
"warp": "",
"xrayTemplateConfig": xrayTemplateConfig,
"webListen": "",
"webDomain": "",
"webPort": "2053",
"webCertFile": "",
"webKeyFile": "",
"secret": random.Seq(32),
"webBasePath": "/",
"sessionMaxAge": "60",
"pageSize": "50",
"expireDiff": "0",
"trafficDiff": "0",
"remarkModel": "-ieo",
"timeLocation": "Local",
"tgBotEnable": "false",
"tgBotToken": "",
"tgBotProxy": "",
"tgBotAPIServer": "",
"tgBotChatId": "",
"tgRunTime": "@daily",
"tgBotBackup": "false",
"tgBotLoginNotify": "true",
"tgCpu": "80",
"tgLang": "en-US",
"secretEnable": "false",
"subEnable": "false",
"subTitle": "",
"subListen": "",
"subPort": "2096",
"subPath": "/sub/",
"subDomain": "",
"subCertFile": "",
"subKeyFile": "",
"subUpdates": "12",
"subEncrypt": "true",
"subShowInfo": "true",
"subURI": "",
"subJsonPath": "/json/",
"subJsonURI": "",
"subJsonFragment": "",
"subJsonNoises": "",
"subJsonMux": "",
"subJsonRules": "",
"datepicker": "gregorian",
"warp": "",
"externalTrafficInformEnable": "false",
"externalTrafficInformURI": "",
}
type SettingService struct{}
func (s *SettingService) GetDefaultJsonConfig() (interface{}, error) {
var jsonData interface{}
func (s *SettingService) GetDefaultJsonConfig() (any, error) {
var jsonData any
err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)
if err != nil {
return nil, err
@@ -416,6 +419,10 @@ func (s *SettingService) GetSubEnable() (bool, error) {
return s.getBool("subEnable")
}
func (s *SettingService) GetSubTitle() (string, error) {
return s.getString("subTitle")
}
func (s *SettingService) GetSubListen() (string, error) {
return s.getString("subListen")
}
@@ -496,6 +503,22 @@ func (s *SettingService) SetWarp(data string) error {
return s.setString("warp", data)
}
func (s *SettingService) GetExternalTrafficInformEnable() (bool, error) {
return s.getBool("externalTrafficInformEnable")
}
func (s *SettingService) SetExternalTrafficInformEnable(value bool) error {
return s.setBool("externalTrafficInformEnable", value)
}
func (s *SettingService) GetExternalTrafficInformURI() (string, error) {
return s.getString("externalTrafficInformURI")
}
func (s *SettingService) SetExternalTrafficInformURI(InformURI string) error {
return s.setString("externalTrafficInformURI", InformURI)
}
func (s *SettingService) GetIpLimitEnable() (bool, error) {
accessLogPath, err := xray.GetAccessLogPath()
if err != nil {
@@ -525,8 +548,8 @@ func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
return common.Combine(errs...)
}
func (s *SettingService) GetDefaultXrayConfig() (interface{}, error) {
var jsonData interface{}
func (s *SettingService) GetDefaultXrayConfig() (any, error) {
var jsonData any
err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)
if err != nil {
return nil, err
@@ -534,24 +557,25 @@ func (s *SettingService) GetDefaultXrayConfig() (interface{}, error) {
return jsonData, nil
}
func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
type settingFunc func() (interface{}, error)
func (s *SettingService) GetDefaultSettings(host string) (any, error) {
type settingFunc func() (any, error)
settings := map[string]settingFunc{
"expireDiff": func() (interface{}, error) { return s.GetExpireDiff() },
"trafficDiff": func() (interface{}, error) { return s.GetTrafficDiff() },
"pageSize": func() (interface{}, error) { return s.GetPageSize() },
"defaultCert": func() (interface{}, error) { return s.GetCertFile() },
"defaultKey": func() (interface{}, error) { return s.GetKeyFile() },
"tgBotEnable": func() (interface{}, error) { return s.GetTgbotEnabled() },
"subEnable": func() (interface{}, error) { return s.GetSubEnable() },
"subURI": func() (interface{}, error) { return s.GetSubURI() },
"subJsonURI": func() (interface{}, error) { return s.GetSubJsonURI() },
"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },
"datepicker": func() (interface{}, error) { return s.GetDatepicker() },
"ipLimitEnable": func() (interface{}, error) { return s.GetIpLimitEnable() },
"expireDiff": func() (any, error) { return s.GetExpireDiff() },
"trafficDiff": func() (any, error) { return s.GetTrafficDiff() },
"pageSize": func() (any, error) { return s.GetPageSize() },
"defaultCert": func() (any, error) { return s.GetCertFile() },
"defaultKey": func() (any, error) { return s.GetKeyFile() },
"tgBotEnable": func() (any, error) { return s.GetTgbotEnabled() },
"subEnable": func() (any, error) { return s.GetSubEnable() },
"subTitle": func() (any, error) { return s.GetSubTitle() },
"subURI": func() (any, error) { return s.GetSubURI() },
"subJsonURI": func() (any, error) { return s.GetSubJsonURI() },
"remarkModel": func() (any, error) { return s.GetRemarkModel() },
"datepicker": func() (any, error) { return s.GetDatepicker() },
"ipLimitEnable": func() (any, error) { return s.GetIpLimitEnable() },
}
result := make(map[string]interface{})
result := make(map[string]any)
for key, fn := range settings {
value, err := fn()
@@ -563,6 +587,7 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
if result["subEnable"].(bool) && (result["subURI"].(string) == "" || result["subJsonURI"].(string) == "") {
subURI := ""
subTitle, _ := s.GetSubTitle()
subPort, _ := s.GetSubPort()
subPath, _ := s.GetSubPath()
subJsonPath, _ := s.GetSubJsonPath()
@@ -589,6 +614,9 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
if result["subURI"].(string) == "" {
result["subURI"] = subURI + subPath
}
if result["subTitle"].(string) == "" {
result["subTitle"] = subTitle
}
if result["subJsonURI"].(string) == "" {
result["subJsonURI"] = subURI + subJsonPath
}

View File

@@ -20,6 +20,8 @@ import (
"x-ui/web/locale"
"x-ui/xray"
"slices"
"github.com/mymmrac/telego"
th "github.com/mymmrac/telego/telegohandler"
tu "github.com/mymmrac/telego/telegoutil"
@@ -307,8 +309,6 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
onlyMessage = true
if isAdmin {
if len(commandArgs) == 0 {
msg += t.I18nBot("tgbot.commands.restartUsage")
} else if strings.ToLower(commandArgs[0]) == "force" {
if t.xrayService.IsXrayRunning() {
err := t.xrayService.RestartXray(true)
if err != nil {
@@ -896,12 +896,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
}
func checkAdmin(tgId int64) bool {
for _, adminId := range adminIds {
if adminId == tgId {
return true
}
}
return false
return slices.Contains(adminIds, tgId)
}
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
@@ -1694,12 +1689,7 @@ func (t *Tgbot) notifyExhausted() {
}
func int64Contains(slice []int64, item int64) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
return slices.Contains(slice, item)
}
func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {

View File

@@ -46,7 +46,7 @@ func (s *UserService) UpdateUser(id int, username string, password string) error
db := database.GetDB()
return db.Model(model.User{}).
Where("id = ?", id).
Updates(map[string]interface{}{"username": username, "password": password}).
Updates(map[string]any{"username": username, "password": password}).
Error
}

View File

@@ -56,8 +56,7 @@ func (s *WarpService) GetWarpConfig() (string, error) {
return "", err
}
defer resp.Body.Close()
buffer := bytes.NewBuffer(make([]byte, 8192))
buffer.Reset()
buffer := &bytes.Buffer{}
_, err = buffer.ReadFrom(resp.Body)
if err != nil {
return "", err
@@ -87,14 +86,13 @@ func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error
return "", err
}
defer resp.Body.Close()
buffer := bytes.NewBuffer(make([]byte, 8192))
buffer.Reset()
buffer := &bytes.Buffer{}
_, err = buffer.ReadFrom(resp.Body)
if err != nil {
return "", err
}
var rspData map[string]interface{}
var rspData map[string]any
err = json.Unmarshal(buffer.Bytes(), &rspData)
if err != nil {
return "", err
@@ -102,7 +100,7 @@ func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error
deviceId := rspData["id"].(string)
token := rspData["token"].(string)
license, ok := rspData["account"].(map[string]interface{})["license"].(string)
license, ok := rspData["account"].(map[string]any)["license"].(string)
if !ok {
logger.Debug("Error accessing license value.")
return "", err
@@ -144,21 +142,20 @@ func (s *WarpService) SetWarpLicense(license string) (string, error) {
return "", err
}
defer resp.Body.Close()
buffer := bytes.NewBuffer(make([]byte, 8192))
buffer.Reset()
buffer := &bytes.Buffer{}
_, err = buffer.ReadFrom(resp.Body)
if err != nil {
return "", err
}
var response map[string]interface{}
var response map[string]any
err = json.Unmarshal(buffer.Bytes(), &response)
if err != nil {
return "", err
}
if response["success"] == false {
errorArr, _ := response["errors"].([]interface{})
errorObj := errorArr[0].(map[string]interface{})
errorArr, _ := response["errors"].([]any)
errorObj := errorArr[0].(map[string]any)
return "", common.NewError(errorObj["code"], errorObj["message"])
}

View File

@@ -56,7 +56,7 @@ func (s *XrayService) GetXrayVersion() string {
return p.GetVersion()
}
func RemoveIndex(s []interface{}, index int) []interface{} {
func RemoveIndex(s []any, index int) []any {
return append(s[:index], s[index+1:]...)
}
@@ -83,16 +83,16 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
continue
}
// get settings clients
settings := map[string]interface{}{}
settings := map[string]any{}
json.Unmarshal([]byte(inbound.Settings), &settings)
clients, ok := settings["clients"].([]interface{})
clients, ok := settings["clients"].([]any)
if ok {
// check users active or not
clientStats := inbound.ClientStats
for _, clientTraffic := range clientStats {
indexDecrease := 0
for index, client := range clients {
c := client.(map[string]interface{})
c := client.(map[string]any)
if c["email"] == clientTraffic.Email {
if !clientTraffic.Enable {
clients = RemoveIndex(clients, index-indexDecrease)
@@ -104,9 +104,9 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
}
// clear client config for additional parameters
var final_clients []interface{}
var final_clients []any
for _, client := range clients {
c := client.(map[string]interface{})
c := client.(map[string]any)
if c["enable"] != nil {
if enable, ok := c["enable"].(bool); ok && !enable {
continue
@@ -120,7 +120,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
c["flow"] = "xtls-rprx-vision"
}
}
final_clients = append(final_clients, interface{}(c))
final_clients = append(final_clients, any(c))
}
settings["clients"] = final_clients
@@ -134,12 +134,12 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
if len(inbound.StreamSettings) > 0 {
// Unmarshal stream JSON
var stream map[string]interface{}
var stream map[string]any
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
// Remove the "settings" field under "tlsSettings" and "realitySettings"
tlsSettings, ok1 := stream["tlsSettings"].(map[string]interface{})
realitySettings, ok2 := stream["realitySettings"].(map[string]interface{})
tlsSettings, ok1 := stream["tlsSettings"].(map[string]any)
realitySettings, ok2 := stream["realitySettings"].(map[string]any)
if ok1 || ok2 {
if ok1 {
delete(tlsSettings, "settings")

View File

@@ -44,6 +44,7 @@
"monitor" = "Listen IP"
"certificate" = "Digital Certificate"
"fail" = "Failed"
"comment" = "Comment"
"success" = "Successfully"
"getVersion" = "Get Version"
"install" = "Install"
@@ -60,8 +61,15 @@
"secAlertPanelURI" = "Panel default URI path is insecure. Please configure a complex URI path."
"secAlertSubURI" = "Subscription default URI path is insecure. Please configure a complex URI path."
"secAlertSubJsonURI" = "Subscription JSON default URI path is insecure. Please configure a complex URI path."
"emptyDnsDesc" = "No added DNS servers."
"emptyFakeDnsDesc" = "No added Fake DNS servers."
"emptyBalancersDesc" = "No added balancers."
"emptyReverseDesc" = "No added reverse proxies."
[menu]
"theme" = "Theme"
"dark" = "Dark"
"ultraDark" = "Ultra Dark"
"dashboard" = "Overview"
"inbounds" = "Inbounds"
"settings" = "Panel Settings"
@@ -94,23 +102,26 @@
"operationHours" = "Uptime"
"systemLoad" = "System Load"
"systemLoadDesc" = "System load average for the past 1, 5, and 15 minutes"
"connectionTcpCountDesc" = "Total TCP connections across the system"
"connectionUdpCountDesc" = "Total UDP connections across the system"
"connectionCount" = "Connection Stats"
"upSpeed" = "Overall upload speed across the system"
"downSpeed" = "Overall download speed across the system"
"totalSent" = "Total data sent across the system since OS startup"
"totalReceive" = "Total data received across the system since OS startup"
"ipAddresses" = "IP Addresses"
"toggleIpVisibility" = "Toggle visibility of the IP"
"overallSpeed" = "Overall Speed"
"upload" = "Upload"
"download" = "Download"
"totalData" = "Total Data"
"sent" = "Sent"
"received" = "Received"
"xraySwitchVersionDialog" = "Change Xray Version"
"xraySwitchVersionDialogDesc" = "Are you sure you want to change the Xray version to"
"dontRefresh" = "Installation is in progress, please do not refresh this page"
"logs" = "Logs"
"config" = "Config"
"backup" = "Backup & Restore"
"backup" = "Backup"
"backupTitle" = "Database Backup & Restore"
"backupDescription" = "It is recommended to make a backup before restoring a database."
"exportDatabase" = "Back Up"
"exportDatabaseDesc" = "Click to download a .db file containing a backup of your current database to your device."
"importDatabase" = "Restore"
"importDatabaseDesc" = "Click to select and upload a .db file from your device to restore your database from a backup."
[pages.inbounds]
"title" = "Inbounds"
@@ -129,6 +140,8 @@
"resetTraffic" = "Reset Traffic"
"addInbound" = "Add Inbound"
"generalActions" = "General Actions"
"autoRefresh" = "Auto-refresh"
"autoRefreshInterval" = "Interval"
"create" = "Create"
"update" = "Update"
"modifyInbound" = "Modify Inbound"
@@ -286,6 +299,8 @@
"subSettings" = "Subscription"
"subEnable" = "Enable Subscription Service"
"subEnableDesc" = "Enables the subscription service."
"subTitle" = "Subscription Title"
"subTitleDesc" = "Title shown in VPN client"
"subListen" = "Listen IP"
"subListenDesc" = "The IP address for the subscription service. (leave blank to listen on all IPs)"
"subPort" = "Listen Port"
@@ -306,6 +321,10 @@
"subShowInfoDesc" = "The remaining traffic and date will be displayed in the client apps."
"subURI" = "Reverse Proxy URI"
"subURIDesc" = "The URI path of the subscription URL for use behind proxies."
"externalTrafficInformEnable" = "External Traffic Inform"
"externalTrafficInformEnableDesc" = "Inform external API on every traffic update."
"externalTrafficInformURI" = "External Traffic Inform URI"
"externalTrafficInformURIDesc" = "Traffic updates are sent to this URI."
"fragment" = "Fragmentation"
"fragmentDesc" = "Enable fragmentation for TLS hello packet."
"fragmentSett" = "Fragmentation Settings"
@@ -316,7 +335,15 @@
"muxSett" = "Mux Settings"
"direct" = "Direct Connection"
"directDesc" = "Directly establishes connections with domains or IP ranges of a specific country."
"notifications" = "Notifications"
"certs" = "Certificaties"
"externalTraffic" = "External Traffic"
"dateAndTime" = "Date and Time"
"proxyAndServer" = "Proxy and Server"
"intervals" = "Intervals"
"information" = "Information"
"language" = "Language"
"telegramBotLanguage" = "Telegram Bot Language"
[pages.xray]
"title" = "Xray Configs"
@@ -369,6 +396,15 @@
"dnsLogDesc" = "Whether to enable DNS query logs"
"maskAddress" = "Mask Address"
"maskAddressDesc" = "IP address mask, when enabled, will automatically replace the IP address that appears in the log."
"statistics" = "Statistics"
"statsInboundUplink" = "Inbound Upload Statistics"
"statsInboundUplinkDesc" = "Enables the statistics collection for upstream traffic of all inbound proxies."
"statsInboundDownlink" = "Inbound Download Statistics"
"statsInboundDownlinkDesc" = "Enables the statistics collection for downstream traffic of all inbound proxies."
"statsOutboundUplink" = "Outbound Upload Statistics"
"statsOutboundUplinkDesc" = "Enables the statistics collection for upstream traffic of all outbound proxies."
"statsOutboundDownlink" = "Outbound Download Statistics"
"statsOutboundDownlinkDesc" = "Enables the statistics collection for downstream traffic of all outbound proxies."
[pages.xray.rules]
"first" = "First"
@@ -398,6 +434,7 @@
"type" = "Type"
"bridge" = "Bridge"
"portal" = "Portal"
"link" = "Link"
"intercon" = "Interconnection"
"settings" = "Settings"
"accountInfo" = "Account Information"
@@ -426,6 +463,14 @@
"enableDesc" = "Enable built-in DNS server"
"tag" = "DNS Inbound Tag"
"tagDesc" = "This tag will be available as an Inbound tag in routing rules."
"clientIp" = "Client IP"
"clientIpDesc" = "Used to notify the server of the specified IP location during DNS queries"
"disableCache" = "Disable cache"
"disableCacheDesc" = "Disables DNS caching"
"disableFallback" = "Disable Fallback"
"disableFallbackDesc" = "Disables fallback DNS queries"
"disableFallbackIfMatch" = "Disable Fallback If Match"
"disableFallbackIfMatchDesc" = "Disables fallback DNS queries when the matching domain list of the DNS server is hit"
"strategy" = "Query Strategy"
"strategyDesc" = "Overall strategy to resolve domain names"
"add" = "Add Server"
@@ -440,7 +485,7 @@
"poolSize" = "Pool Size"
[pages.settings.security]
"admin" = "Admin"
"admin" = "Admin credentials"
"secret" = "Secret Token"
"loginSecurity" = "Secure Login"
"loginSecurityDesc" = "Adds an additional layer of authentication to provide more security."
@@ -483,9 +528,9 @@
"status" = "✅ Bot is OK!"
"usage" = "❗ Please provide a text to search!"
"getID" = "🆔 Your ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "To restart Xray Core:\r\n<code>/restart force</code>\r\n\r\nTo search for a client email:\r\n<code>/usage [Email]</code>\r\n\r\nTo search for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>\r\n\r\nTelegram Chat ID:\r\n<code>/id</code>"
"helpAdminCommands" = "To restart Xray Core:\r\n<code>/restart</code>\r\n\r\nTo search for a client email:\r\n<code>/usage [Email]</code>\r\n\r\nTo search for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>\r\n\r\nTelegram Chat ID:\r\n<code>/id</code>"
"helpClientCommands" = "To search for statistics, use the following command:\r\n\r\n<code>/usage [Email]</code>\r\n\r\nTelegram Chat ID:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart force</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Operation successful!"
"restartFailed" = "❗ Error in operation.\r\n\r\n<code>Error: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core is not running."

View File

@@ -44,6 +44,7 @@
"monitor" = "Listening IP"
"certificate" = "Certificado Digital"
"fail" = "Falló"
"comment" = "Comentario"
"success" = "Éxito"
"getVersion" = "Obtener versión"
"install" = "Instalar"
@@ -60,8 +61,15 @@
"secAlertPanelURI" = "La ruta URI predeterminada del panel no es segura. Por favor, configure una ruta URI compleja."
"secAlertSubURI" = "La ruta URI predeterminada de la suscripción no es segura. Por favor, configure una ruta URI compleja."
"secAlertSubJsonURI" = "La ruta URI JSON predeterminada de la suscripción no es segura. Por favor, configure una ruta URI compleja."
"emptyDnsDesc" = "No hay servidores DNS añadidos."
"emptyFakeDnsDesc" = "No hay servidores Fake DNS añadidos."
"emptyBalancersDesc" = "No hay balanceadores añadidos."
"emptyReverseDesc" = "No hay proxies inversos añadidos."
[menu]
"theme" = "Tema"
"dark" = "Oscuro"
"ultraDark" = "Ultra Oscuro"
"dashboard" = "Estado del Sistema"
"inbounds" = "Entradas"
"settings" = "Configuraciones"
@@ -97,20 +105,25 @@
"connectionTcpCountDesc" = "Conexiones TCP totales en todas las tarjetas de red."
"connectionUdpCountDesc" = "Conexiones UDP totales en todas las tarjetas de red."
"connectionCount" = "Número de Conexiones"
"upSpeed" = "Velocidad de Subida Total para Todas las Tarjetas de Red."
"downSpeed" = "Velocidad de Bajada Total para Todas las Tarjetas de Red."
"totalSent" = "Tráfico Total de Subida de Todas las Tarjetas de Red desde el inicio del sistema."
"totalReceive" = "Datos Descargados Totales en Todas las Tarjetas de Red desde el inicio del sistema."
"ipAddresses" = "Direcciones IP"
"toggleIpVisibility" = "Alternar visibilidad de la IP"
"overallSpeed" = "Velocidad general"
"upload" = "Subida"
"download" = "Descarga"
"totalData" = "Datos totales"
"sent" = "Enviado"
"received" = "Recibido"
"xraySwitchVersionDialog" = "Cambiar Versión de Xray"
"xraySwitchVersionDialogDesc" = "¿Estás seguro de que deseas cambiar la versión de Xray a"
"dontRefresh" = "La instalación está en progreso, por favor no actualices esta página."
"logs" = "Registros"
"config" = "Configuración"
"backup" = "Copia de Seguridad y Restauración"
"backup" = "Сopia de Seguridad"
"backupTitle" = "Copia de Seguridad y Restauración de la Base de Datos"
"backupDescription" = "Recuerda hacer una copia de seguridad antes de importar una nueva base de datos."
"exportDatabase" = "Descargar Base de Datos"
"importDatabase" = "Cargar Base de Datos"
"exportDatabase" = "Copia de seguridad"
"exportDatabaseDesc" = "Haz clic para descargar un archivo .db que contiene una copia de seguridad de tu base de datos actual en tu dispositivo."
"importDatabase" = "Restaurar"
"importDatabaseDesc" = "Haz clic para seleccionar y cargar un archivo .db desde tu dispositivo para restaurar tu base de datos desde una copia de seguridad."
[pages.inbounds]
"title" = "Entradas"
@@ -129,6 +142,8 @@
"resetTraffic" = "Restablecer Tráfico"
"addInbound" = "Agregar Entrada"
"generalActions" = "Acciones Generales"
"autoRefresh" = "Auto-actualizar"
"autoRefreshInterval" = "Intervalo"
"create" = "Crear"
"update" = "Actualizar"
"modifyInbound" = "Modificar Entrada"
@@ -286,6 +301,8 @@
"subSettings" = "Suscripción"
"subEnable" = "Habilitar Servicio"
"subEnableDesc" = "Función de suscripción con configuración separada."
"subTitle" = "Título de la Suscripción"
"subTitleDesc" = "Título mostrado en el cliente de VPN"
"subListen" = "Listening IP"
"subListenDesc" = "Dejar en blanco por defecto para monitorear todas las IPs."
"subPort" = "Puerto de Suscripción"
@@ -305,6 +322,10 @@
"subShowInfo" = "Mostrar información de uso"
"subShowInfoDesc" = "Mostrar tráfico restante y fecha después del nombre de configuración."
"subURI" = "URI de proxy inverso"
"externalTrafficInformEnable" = "Informe de tráfico externo"
"externalTrafficInformEnableDesc" = "Informar a la API externa sobre cada actualización de tráfico."
"externalTrafficInformURI" = "URI de información de tráfico externo"
"externalTrafficInformURIDesc" = "Las actualizaciones de tráfico se envían a este URI."
"subURIDesc" = "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy"
"fragment" = "Fragmentación"
"fragmentDesc" = "Habilitar la fragmentación para el paquete de saludo de TLS"
@@ -316,7 +337,15 @@
"muxSett" = "Configuración Mux"
"direct" = "Conexión Directa"
"directDesc" = "Establece conexiones directas con dominios o rangos de IP de un país específico."
"notifications" = "Notificaciones"
"certs" = "Certificados"
"externalTraffic" = "Tráfico Externo"
"dateAndTime" = "Fecha y Hora"
"proxyAndServer" = "Proxy y Servidor"
"intervals" = "Intervalos"
"information" = "Información"
"language" = "Idioma"
"telegramBotLanguage" = "Idioma del Bot de Telegram"
[pages.xray]
"title" = "Xray Configuración"
@@ -369,6 +398,15 @@
"dnsLogDesc" = "Si habilitar los registros de consulta DNS"
"maskAddress" = "Enmascarar Dirección"
"maskAddressDesc" = "Máscara de dirección IP, cuando se habilita, reemplazará automáticamente la dirección IP que aparece en el registro."
"statistics" = "Estadísticas"
"statsInboundUplink" = "Estadísticas de Subida de Entrada"
"statsInboundUplinkDesc" = "Habilita la recopilación de estadísticas para el tráfico ascendente de todos los proxies de entrada."
"statsInboundDownlink" = "Estadísticas de Bajada de Entrada"
"statsInboundDownlinkDesc" = "Habilita la recopilación de estadísticas para el tráfico descendente de todos los proxies de entrada."
"statsOutboundUplink" = "Estadísticas de Subida de Salida"
"statsOutboundUplinkDesc" = "Habilita la recopilación de estadísticas para el tráfico ascendente de todos los proxies de salida."
"statsOutboundDownlink" = "Estadísticas de Bajada de Salida"
"statsOutboundDownlinkDesc" = "Habilita la recopilación de estadísticas para el tráfico descendente de todos los proxies de salida."
[pages.xray.rules]
"first" = "Primero"
@@ -398,6 +436,7 @@
"type" = "Tipo"
"bridge" = "puente"
"portal" = "portal"
"link" = "Enlace"
"intercon" = "Interconexión"
"settings" = "Configuración"
"accountInfo" = "Información de la Cuenta"
@@ -426,6 +465,14 @@
"enableDesc" = "Habilitar servidor DNS incorporado"
"tag" = "Etiqueta de Entrada DNS"
"tagDesc" = "Esta etiqueta estará disponible como una etiqueta de entrada en las reglas de enrutamiento."
"clientIp" = "IP del cliente"
"clientIpDesc" = "Se utiliza para notificar al servidor la ubicación IP especificada durante las consultas DNS"
"disableCache" = "Desactivar caché"
"disableCacheDesc" = "Desactiva el almacenamiento en caché de DNS"
"disableFallback" = "Desactivar respaldo"
"disableFallbackDesc" = "Desactiva las consultas DNS de respaldo"
"disableFallbackIfMatch" = "Desactivar respaldo si coincide"
"disableFallbackIfMatchDesc" = "Desactiva las consultas DNS de respaldo cuando se acierta en la lista de dominios coincidentes del servidor DNS"
"strategy" = "Estrategia de Consulta"
"strategyDesc" = "Estrategia general para resolver nombres de dominio"
"add" = "Agregar Servidor"
@@ -440,7 +487,7 @@
"poolSize" = "Tamaño del grupo"
[pages.settings.security]
"admin" = "Administrador"
"admin" = "Credenciales de administrador"
"secret" = "Token Secreto"
"loginSecurity" = "Seguridad de Inicio de Sesión"
"loginSecurityDesc" = "Habilitar un paso adicional de seguridad para el inicio de sesión de usuarios."
@@ -483,9 +530,9 @@
"status" = "✅ ¡El bot está bien!"
"usage" = "❗ ¡Por favor proporciona un texto para buscar!"
"getID" = "🆔 Tu ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Para reiniciar Xray Core:\r\n<code>/restart force</code>\r\n\r\nPara buscar un correo electrónico de cliente:\r\n<code>/usage [Correo electrónico]</code>\r\n\r\nPara buscar entradas (con estadísticas de cliente):\r\n<code>/inbound [Observación]</code>\r\n\r\nID de Chat de Telegram:\r\n<code>/id</code>"
"helpAdminCommands" = "Para reiniciar Xray Core:\r\n<code>/restart</code>\r\n\r\nPara buscar un correo electrónico de cliente:\r\n<code>/usage [Correo electrónico]</code>\r\n\r\nPara buscar entradas (con estadísticas de cliente):\r\n<code>/inbound [Observación]</code>\r\n\r\nID de Chat de Telegram:\r\n<code>/id</code>"
"helpClientCommands" = "Para buscar estadísticas, utiliza el siguiente comando:\r\n<code>/usage [Correo electrónico]</code>\r\n\r\nID de Chat de Telegram:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart force</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ ¡Operación exitosa!"
"restartFailed" = "❗ Error en la operación.\r\n\r\n<code>Error: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core no está en ejecución."

View File

@@ -44,6 +44,7 @@
"monitor" = "آی‌پی اتصال"
"certificate" = "گواهی دیجیتال"
"fail" = "ناموفق"
"comment" = "توضیحات"
"success" = "موفق"
"getVersion" = "دریافت نسخه"
"install" = "نصب"
@@ -60,8 +61,15 @@
"secAlertPanelURI" = "مسیر پیش‌فرض لینک پنل ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید"
"secAlertSubURI" = "مسیر پیش‌فرض لینک سابسکریپشن ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید"
"secAlertSubJsonURI" = "مسیر پیش‌فرض لینک سابسکریپشن جیسون ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید"
"emptyDnsDesc" = "هیچ سرور DNS اضافه نشده است."
"emptyFakeDnsDesc" = "هیچ سرور Fake DNS اضافه نشده است."
"emptyBalancersDesc" = "هیچ بالانسر اضافه نشده است."
"emptyReverseDesc" = "هیچ پروکسی معکوس اضافه نشده است."
[menu]
"theme" = "تم"
"dark" = "تیره"
"ultraDark" = "فوق تیره"
"dashboard" = "نمای کلی"
"inbounds" = "ورودی‌ها"
"settings" = "تنظیمات پنل"
@@ -97,10 +105,14 @@
"connectionTcpCountDesc" = "در تمام‌شبکه‌ها TCP مجموع‌اتصالات"
"connectionUdpCountDesc" = "در تمام‌شبکه‌ها UDP مجموع‌اتصالات"
"connectionCount" = "تعداد کانکشن ها"
"upSpeed" = "سرعت کلی آپلود در تمام‌شبکه‌ها"
"downSpeed" = "‌سرعت کلی دانلود در تمام‌شبکه‌ها"
"totalSent" = "مجموع ترافیک ارسال‌‌شده پس‌از شروع‌به‌کار سیستم‌عامل"
"totalReceive" = "مجموع ترافیک دریافت‌شده پس‌از شروع‌به‌کار سیستم‌عامل"
"ipAddresses" = "آدرس‌های IP"
"toggleIpVisibility" = "تغییر وضعیت نمایش IP"
"overallSpeed" = "سرعت کلی"
"upload" = "آپلود"
"download" = "دانلود"
"totalData" = "داده‌های کل"
"sent" = "ارسال شده"
"received" = "دریافت شده"
"xraySwitchVersionDialog" = "تغییر نسخه ایکس‌ری"
"xraySwitchVersionDialogDesc" = "آیا از تغییر نسخه‌ مطمئن هستید؟"
"dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید"
@@ -108,9 +120,10 @@
"config" = "پیکربندی"
"backup" = "پشتیبان‌گیری"
"backupTitle" = "پشتیبان‌گیری دیتابیس"
"backupDescription" = "توصیه‌می‌شود قبل‌از واردکردن یک دیتابیس جدید، نسخه پشتیبان تهیه ‌کنید"
"exportDatabase" = "پشتیبان‌گیری"
"importDatabase" = ازگرداندن"
"exportDatabaseDesc" = رای دانلود یک فایل .db حاوی پشتیبان از پایگاه داده فعلی خود به دستگاهتان کلیک کنید."
"importDatabase" = "بازیابی"
"importDatabaseDesc" = "برای انتخاب و آپلود یک فایل .db از دستگاهتان و بازیابی پایگاه داده از یک پشتیبان کلیک کنید."
[pages.inbounds]
"title" = "کاربران"
@@ -129,6 +142,8 @@
"resetTraffic" = "ریست ترافیک"
"addInbound" = "افزودن ورودی"
"generalActions" = "عملیات کلی"
"autoRefresh" = "تازه‌سازی خودکار"
"autoRefreshInterval" = "فاصله"
"create" = "افزودن"
"update" = "ویرایش"
"modifyInbound" = "ویرایش ورودی"
@@ -286,6 +301,8 @@
"subSettings" = "سابسکریپشن"
"subEnable" = "فعال‌سازی سرویس سابسکریپشن"
"subEnableDesc" = "سرویس سابسکریپشن‌ را فعال‌می‌کند"
"subTitle" = "عنوان اشتراک"
"subTitleDesc" = "عنوان نمایش داده شده در کلاینت VPN"
"subListen" = "آدرس آی‌پی"
"subListenDesc" = "آدرس آی‌پی برای سرویس سابسکریپشن. برای گوش دادن به‌تمام آی‌پی‌ها خالی‌بگذارید"
"subPort" = "پورت"
@@ -300,6 +317,10 @@
"subDomainDesc" = "آدرس دامنه برای سرویس سابسکریپشن. برای گوش دادن به تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید‌"
"subUpdates" = "فاصله بروزرسانی‌ سابسکریپشن"
"subUpdatesDesc" = "(فاصله مابین بروزرسانی در برنامه‌های کاربری. (واحد: ساعت"
"externalTrafficInformEnable" = "اطلاع رسانی خارجی مصرف ترافیک"
"externalTrafficInformEnableDesc" = "مصرف ترافیک به سرویس خارجی ارسال می شود"
"externalTrafficInformURI" = "لینک اطلاع رسانی خارجی مصرف ترافیک"
"externalTrafficInformURIDesc" = "ترافیک های مصرفی به این لینک هم ارسال می شود"
"subEncrypt" = "کدگذاری"
"subEncryptDesc" = "کدگذاری خواهدشد Base64 محتوای برگشتی سرویس سابسکریپشن برپایه"
"subShowInfo" = "نمایش اطلاعات مصرف"
@@ -316,7 +337,15 @@
"muxSett" = "تنظیمات ماکس"
"direct" = "اتصال مستقیم"
"directDesc" = "به طور مستقیم با دامنه ها یا محدوده آی‌پی یک کشور خاص ارتباط برقرار می کند"
"notifications" = "اعلان‌ها"
"certs" = "گواهی‌ها"
"externalTraffic" = "ترافیک خارجی"
"dateAndTime" = "تاریخ و زمان"
"proxyAndServer" = "پراکسی و سرور"
"intervals" = "فواصل"
"information" = "اطلاعات"
"language" = "زبان"
"telegramBotLanguage" = "زبان ربات تلگرام"
[pages.xray]
"title" = "پیکربندی ایکس‌ری"
@@ -369,6 +398,15 @@
"dnsLogDesc" = "آیا ثبت‌های درخواست DNS را فعال کنید"
"maskAddress" = "پنهان کردن آدرس"
"maskAddressDesc" = "پوشش آدرس IP، هنگامی که فعال می‌شود، به طور خودکار آدرس IP که در لاگ ظاهر می‌شود را جایگزین می‌کند."
"statistics" = "آمار"
"statsInboundUplink" = "آمار آپلود ورودی"
"statsInboundUplinkDesc" = "جمع‌آوری آمار برای ترافیک بالارو (آپلود) تمام پروکسی‌های ورودی را فعال می‌کند."
"statsInboundDownlink" = "آمار دانلود ورودی"
"statsInboundDownlinkDesc" = "جمع‌آوری آمار برای ترافیک پایین‌رو (دانلود) تمام پروکسی‌های ورودی را فعال می‌کند."
"statsOutboundUplink" = "آمار آپلود خروجی"
"statsOutboundUplinkDesc" = "جمع‌آوری آمار برای ترافیک بالارو (آپلود) تمام پروکسی‌های خروجی را فعال می‌کند."
"statsOutboundDownlink" = "آمار دانلود خروجی"
"statsOutboundDownlinkDesc" = "جمع‌آوری آمار برای ترافیک پایین‌رو (دانلود) تمام پروکسی‌های خروجی را فعال می‌کند."
[pages.xray.rules]
"first" = "اولین"
@@ -398,6 +436,7 @@
"type" = "نوع"
"bridge" = "پل"
"portal" = "پورتال"
"link" = "لینک"
"intercon" = "اتصال میانی"
"settings" = "تنظیمات"
"accountInfo" = "اطلاعات حساب"
@@ -426,6 +465,14 @@
"enableDesc" = "سرور حل دامنه داخلی را فعال کنید"
"tag" = "برچسب"
"tagDesc" = "این برچسب در قوانین مسیریابی به عنوان یک برچسب ورودی قابل استفاده خواهد بود"
"clientIp" = "آی‌پی کلاینت"
"clientIpDesc" = "برای اطلاع‌رسانی به سرور درباره مکان IP مشخص‌شده در طول درخواست‌های DNS استفاده می‌شود"
"disableCache" = "غیرفعال‌سازی کش"
"disableCacheDesc" = "کش DNS را غیرفعال می‌کند"
"disableFallback" = "غیرفعال‌سازی Fallback"
"disableFallbackDesc" = "درخواست‌های DNS Fallback را غیرفعال می‌کند"
"disableFallbackIfMatch" = "غیرفعال‌سازی Fallback در صورت تطابق"
"disableFallbackIfMatchDesc" = "درخواست‌های DNS Fallback را زمانی که لیست دامنه‌های مطابقت‌یافته سرور DNS فعال است، غیرفعال می‌کند"
"strategy" = "استراتژی پرس‌وجو"
"strategyDesc" = "استراتژی کلی برای حل نام دامنه"
"add" = "افزودن سرور"
@@ -440,7 +487,7 @@
"poolSize" = "اندازه استخر"
[pages.settings.security]
"admin" = "مدیر"
"admin" = "اعتبارنامه‌های ادمین"
"secret" = "توکن مخفی"
"loginSecurity" = "ورود ایمن"
"loginSecurityDesc" = "یک لایه اضافی از احراز هویت برای ایجاد امنیت بیشتر اضافه می کند"
@@ -483,9 +530,9 @@
"status" = "✅ ربات در حالت عادی است!"
"usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!"
"getID" = "🆔 شناسه شما: <code>{{ .ID }}</code>"
"helpAdminCommands" = "برای راه‌اندازی مجدد Xray Core:\r\n<code>/restart force</code>\r\n\r\nبرای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n\r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n<code>/inbound [توضیحات]</code>\r\n\r\nشناسه گفتگوی تلگرام:\r\n<code>/id</code>"
"helpAdminCommands" = "برای راه‌اندازی مجدد Xray Core:\r\n<code>/restart</code>\r\n\r\nبرای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n\r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n<code>/inbound [توضیحات]</code>\r\n\r\nشناسه گفتگوی تلگرام:\r\n<code>/id</code>"
"helpClientCommands" = "برای جستجوی آمار، از دستور زیر استفاده کنید:\r\n<code>/usage [ایمیل]</code>\r\n\r\nشناسه گفتگوی تلگرام:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart force</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ عملیات با موفقیت انجام شد!"
"restartFailed" = "❗ خطا در عملیات.\r\n\r\n<code>خطا: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core در حال اجرا نیست."

View File

@@ -44,6 +44,7 @@
"monitor" = "IP Pemantauan"
"certificate" = "Sertifikat Digital"
"fail" = "Gagal"
"comment" = "Komentar"
"success" = "Berhasil"
"getVersion" = "Dapatkan Versi"
"install" = "Instal"
@@ -60,8 +61,15 @@
"secAlertPanelURI" = "Jalur URI default panel tidak aman. Harap konfigurasi jalur URI kompleks."
"secAlertSubURI" = "Jalur URI default langganan tidak aman. Harap konfigurasi jalur URI kompleks."
"secAlertSubJsonURI" = "Jalur URI default JSON langganan tidak aman. Harap konfigurasikan jalur URI kompleks."
"emptyDnsDesc" = "Tidak ada server DNS yang ditambahkan."
"emptyFakeDnsDesc" = "Tidak ada server Fake DNS yang ditambahkan."
"emptyBalancersDesc" = "Tidak ada penyeimbang yang ditambahkan."
"emptyReverseDesc" = "Tidak ada proxy terbalik yang ditambahkan."
[menu]
"theme" = "Tema"
"dark" = "Gelap"
"ultraDark" = "Sangat Gelap"
"dashboard" = "Ikhtisar"
"inbounds" = "Masuk"
"settings" = "Pengaturan Panel"
@@ -97,20 +105,25 @@
"connectionTcpCountDesc" = "Total koneksi TCP di seluruh sistem"
"connectionUdpCountDesc" = "Total koneksi UDP di seluruh sistem"
"connectionCount" = "Statistik Koneksi"
"upSpeed" = "Kecepatan unggah keseluruhan di seluruh sistem"
"downSpeed" = "Kecepatan unduh keseluruhan di seluruh sistem"
"totalSent" = "Total data terkirim di seluruh sistem sejak startup OS"
"totalReceive" = "Total data diterima di seluruh sistem sejak startup OS"
"ipAddresses" = "Alamat IP"
"toggleIpVisibility" = "Alihkan visibilitas IP"
"overallSpeed" = "Kecepatan keseluruhan"
"upload" = "Unggah"
"download" = "Unduh"
"totalData" = "Total data"
"sent" = "Dikirim"
"received" = "Diterima"
"xraySwitchVersionDialog" = "Ganti Versi Xray"
"xraySwitchVersionDialogDesc" = "Apakah Anda yakin ingin mengubah versi Xray menjadi"
"dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini"
"logs" = "Log"
"config" = "Konfigurasi"
"backup" = "Cadangan & Pulihkan"
"backup" = "Cadangan"
"backupTitle" = "Cadangan & Pulihkan Database"
"backupDescription" = "Disarankan untuk membuat cadangan sebelum memulihkan database."
"exportDatabase" = "Cadangkan"
"exportDatabaseDesc" = "Klik untuk mengunduh file .db yang berisi cadangan dari database Anda saat ini ke perangkat Anda."
"importDatabase" = "Pulihkan"
"importDatabaseDesc" = "Klik untuk memilih dan mengunggah file .db dari perangkat Anda untuk memulihkan database dari cadangan."
[pages.inbounds]
"title" = "Masuk"
@@ -129,6 +142,8 @@
"resetTraffic" = "Reset Traffic"
"addInbound" = "Tambahkan Masuk"
"generalActions" = "Tindakan Umum"
"autoRefresh" = "Pembaruan otomatis"
"autoRefreshInterval" = "Interval"
"create" = "Buat"
"update" = "Perbarui"
"modifyInbound" = "Ubah Masuk"
@@ -286,6 +301,8 @@
"subSettings" = "Langganan"
"subEnable" = "Aktifkan Layanan Langganan"
"subEnableDesc" = "Mengaktifkan layanan langganan."
"subTitle" = "Judul Langganan"
"subTitleDesc" = "Judul yang ditampilkan di klien VPN"
"subListen" = "IP Pendengar"
"subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)"
"subPort" = "Port Pendengar"
@@ -305,7 +322,10 @@
"subShowInfo" = "Tampilkan Info Penggunaan"
"subShowInfoDesc" = "Sisa traffic dan tanggal akan ditampilkan di aplikasi klien."
"subURI" = "URI Proxy Terbalik"
"subURIDesc" = "URI path URL langganan untuk penggunaan di belakang proxy."
"externalTrafficInformEnable" = "Informasikan API eksternal pada setiap pembaruan lalu lintas."
"externalTrafficInformEnableDesc" = "Inform external API on every traffic update."
"externalTrafficInformURI" = "Lalu Lintas Eksternal Menginformasikan URI"
"externalTrafficInformURIDesc" = "Pembaruan lalu lintas dikirim ke URI ini."
"fragment" = "Fragmentasi"
"fragmentDesc" = "Aktifkan fragmentasi untuk paket hello TLS"
"fragmentSett" = "Pengaturan Fragmentasi"
@@ -316,7 +336,15 @@
"muxSett" = "Pengaturan Mux"
"direct" = "Koneksi langsung"
"directDesc" = "Secara langsung membuat koneksi dengan domain atau rentang IP negara tertentu."
"notifications" = "Notifikasi"
"certs" = "Sertifikat"
"externalTraffic" = "Lalu Lintas Eksternal"
"dateAndTime" = "Tanggal dan Waktu"
"proxyAndServer" = "Proxy dan Server"
"intervals" = "Interval"
"information" = "Informasi"
"language" = "Bahasa"
"telegramBotLanguage" = "Bahasa Bot Telegram"
[pages.xray]
"title" = "Konfigurasi Xray"
@@ -369,6 +397,15 @@
"dnsLogDesc" = "Apakah akan mengaktifkan log kueri DNS"
"maskAddress" = "Alamat Masker"
"maskAddressDesc" = "Masker alamat IP, ketika diaktifkan, akan secara otomatis mengganti alamat IP yang muncul di log."
"statistics" = "Statistik"
"statsInboundUplink" = "Statistik Unggah Masuk"
"statsInboundUplinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unggah dari semua proxy masuk."
"statsInboundDownlink" = "Statistik Unduh Masuk"
"statsInboundDownlinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unduh dari semua proxy masuk."
"statsOutboundUplink" = "Statistik Unggah Keluar"
"statsOutboundUplinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unggah dari semua proxy keluar."
"statsOutboundDownlink" = "Statistik Unduh Keluar"
"statsOutboundDownlinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unduh dari semua proxy keluar."
[pages.xray.rules]
"first" = "Pertama"
@@ -398,6 +435,7 @@
"type" = "Tipe"
"bridge" = "Jembatan"
"portal" = "Portal"
"link" = "Tautan"
"intercon" = "Interkoneksi"
"settings" = "Pengaturan"
"accountInfo" = "Informasi Akun"
@@ -426,6 +464,14 @@
"enableDesc" = "Aktifkan server DNS bawaan"
"tag" = "Tanda DNS Masuk"
"tagDesc" = "Tanda ini akan tersedia sebagai tanda masuk dalam aturan penataan."
"clientIp" = "IP Klien"
"clientIpDesc" = "Digunakan untuk memberi tahu server tentang lokasi IP yang ditentukan selama kueri DNS"
"disableCache" = "Nonaktifkan cache"
"disableCacheDesc" = "Menonaktifkan caching DNS"
"disableFallback" = "Nonaktifkan Fallback"
"disableFallbackDesc" = "Menonaktifkan kueri DNS fallback"
"disableFallbackIfMatch" = "Nonaktifkan Fallback Jika Cocok"
"disableFallbackIfMatchDesc" = "Menonaktifkan kueri DNS fallback ketika daftar domain yang cocok dari server DNS terpenuhi"
"strategy" = "Strategi Kueri"
"strategyDesc" = "Strategi keseluruhan untuk menyelesaikan nama domain"
"add" = "Tambahkan Server"
@@ -440,7 +486,7 @@
"poolSize" = "Ukuran Kolam"
[pages.settings.security]
"admin" = "Admin"
"admin" = "Kredensial admin"
"secret" = "Token Rahasia"
"loginSecurity" = "Login Aman"
"loginSecurityDesc" = "Menambahkan lapisan otentikasi tambahan untuk memberikan keamanan lebih."
@@ -483,9 +529,9 @@
"status" = "✅ Bot dalam keadaan baik!"
"usage" = "❗ Harap berikan teks untuk mencari!"
"getID" = "🆔 ID Anda: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Untuk memulai ulang Xray Core:\r\n<code>/restart force</code>\r\n\r\nUntuk mencari email klien:\r\n<code>/usage [Email]</code>\r\n\r\nUntuk mencari inbound (dengan statistik klien):\r\n<code>/inbound [Catatan]</code>\r\n\r\nID Obrolan Telegram:\r\n<code>/id</code>"
"helpAdminCommands" = "Untuk memulai ulang Xray Core:\r\n<code>/restart</code>\r\n\r\nUntuk mencari email klien:\r\n<code>/usage [Email]</code>\r\n\r\nUntuk mencari inbound (dengan statistik klien):\r\n<code>/inbound [Catatan]</code>\r\n\r\nID Obrolan Telegram:\r\n<code>/id</code>"
"helpClientCommands" = "Untuk mencari statistik, gunakan perintah berikut:\r\n<code>/usage [Email]</code>\r\n\r\nID Obrolan Telegram:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart force</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Operasi berhasil!"
"restartFailed" = "❗ Kesalahan dalam operasi.\r\n\r\n<code>Error: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core tidak berjalan."

View File

@@ -44,6 +44,7 @@
"monitor" = "監視"
"certificate" = "証明書"
"fail" = "失敗"
"comment" = "コメント"
"success" = "成功"
"getVersion" = "バージョン取得"
"install" = "インストール"
@@ -60,8 +61,15 @@
"secAlertPanelURI" = "デフォルトのURIパスは安全ではありません。複雑なURIパスを設定してください。"
"secAlertSubURI" = "サブスクリプションのデフォルトURIパスは安全ではありません。複雑なURIパスを設定してください。"
"secAlertSubJsonURI" = "JSONサブスクリプションのデフォルトURIパスは安全ではありません。複雑なURIパスを設定してください。"
"emptyDnsDesc" = "追加されたDNSサーバーはありません。"
"emptyFakeDnsDesc" = "追加されたFake DNSサーバーはありません。"
"emptyBalancersDesc" = "追加されたバランサーはありません。"
"emptyReverseDesc" = "追加されたリバースプロキシはありません。"
[menu]
"theme" = "テーマ"
"dark" = "ダーク"
"ultraDark" = "ウルトラダーク"
"dashboard" = "ダッシュボード"
"inbounds" = "インバウンド一覧"
"settings" = "パネル設定"
@@ -97,20 +105,25 @@
"connectionTcpCountDesc" = "システム内のすべてのTCP接続数"
"connectionUdpCountDesc" = "システム内のすべてのUDP接続数"
"connectionCount" = "接続数"
"upSpeed" = "総アップロード速度"
"downSpeed" = "総ダウンロード速度"
"totalSent" = "システム起動以降の送信データ量"
"totalReceive" = "システム起動以降の受信データ量"
"ipAddresses" = "IPアドレス"
"toggleIpVisibility" = "IPの表示を切り替える"
"overallSpeed" = "全体の速度"
"upload" = "アップロード"
"download" = "ダウンロード"
"totalData" = "総データ量"
"sent" = "送信"
"received" = "受信"
"xraySwitchVersionDialog" = "Xrayバージョン切り替え"
"xraySwitchVersionDialogDesc" = "Xrayのバージョンを切り替えますか"
"dontRefresh" = "インストール中、このページをリロードしないでください"
"logs" = "ログ"
"config" = "設定"
"backup" = "バックアップと復元"
"backup" = "バックアップ"
"backupTitle" = "データベースのバックアップと復元"
"backupDescription" = "データベースを復元する前にバックアップすることをお勧めします"
"exportDatabase" = "バックアップ"
"exportDatabaseDesc" = "クリックして、現在のデータベースのバックアップを含む .db ファイルをデバイスにダウンロードします。"
"importDatabase" = "復元"
"importDatabaseDesc" = "クリックして、デバイスから .db ファイルを選択し、アップロードしてバックアップからデータベースを復元します。"
[pages.inbounds]
"title" = "インバウンド一覧"
@@ -129,6 +142,8 @@
"resetTraffic" = "トラフィックリセット"
"addInbound" = "インバウンド追加"
"generalActions" = "一般操作"
"autoRefresh" = "自動更新"
"autoRefreshInterval" = "間隔"
"create" = "追加"
"update" = "更新"
"modifyInbound" = "インバウンド修正"
@@ -286,6 +301,8 @@
"subSettings" = "サブスクリプション設定"
"subEnable" = "サブスクリプションサービスを有効にする"
"subEnableDesc" = "サブスクリプションサービス機能を有効にする"
"subTitle" = "サブスクリプションタイトル"
"subTitleDesc" = "VPNクライアントに表示されるタイトル"
"subListen" = "監視IP"
"subListenDesc" = "サブスクリプションサービスが監視するIPアドレス空白にするとすべてのIPを監視"
"subPort" = "監視ポート"
@@ -306,6 +323,10 @@
"subShowInfoDesc" = "クライアントアプリで残りのトラフィックと日付情報を表示する"
"subURI" = "リバースプロキシURI"
"subURIDesc" = "プロキシ後ろのサブスクリプションURLのURIパスに使用する"
"externalTrafficInformEnable" = "外部トラフィック情報"
"externalTrafficInformEnableDesc" = "トラフィックの更新ごとに外部 API に通知します。"
"externalTrafficInformURI" = "外部トラフィック通知 URI"
"externalTrafficInformURIDesc" = "トラフィックの更新ごとに外部 API に通知します。"
"fragment" = "フラグメント"
"fragmentDesc" = "TLS helloパケットのフラグメントを有効にする"
"fragmentSett" = "設定"
@@ -316,7 +337,15 @@
"muxSett" = "マルチプレクサ設定"
"direct" = "直接接続"
"directDesc" = "特定の国のドメインまたはIP範囲に直接接続する"
"notifications" = "通知"
"certs" = "証明書"
"externalTraffic" = "外部トラフィック"
"dateAndTime" = "日付と時刻"
"proxyAndServer" = "プロキシとサーバー"
"intervals" = "間隔"
"information" = "情報"
"language" = "言語"
"telegramBotLanguage" = "Telegram Botの言語"
[pages.xray]
"title" = "Xray 設定"
@@ -369,6 +398,15 @@
"dnsLogDesc" = "DNSクエリのログを有効にするかどうか"
"maskAddress" = "アドレスをマスク"
"maskAddressDesc" = "IPアドレスをマスクし、有効にするとログに表示されるIPアドレスを自動的に置き換えます"
"statistics" = "統計"
"statsInboundUplink" = "インバウンドアップロード統計"
"statsInboundUplinkDesc" = "すべてのインバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。"
"statsInboundDownlink" = "インバウンドダウンロード統計"
"statsInboundDownlinkDesc" = "すべてのインバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。"
"statsOutboundUplink" = "アウトバウンドアップロード統計"
"statsOutboundUplinkDesc" = "すべてのアウトバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。"
"statsOutboundDownlink" = "アウトバウンドダウンロード統計"
"statsOutboundDownlinkDesc" = "すべてのアウトバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。"
[pages.xray.rules]
"first" = "最初"
@@ -398,6 +436,7 @@
"type" = "タイプ"
"bridge" = "ブリッジ"
"portal" = "ポータル"
"link" = "リンク"
"intercon" = "インターコネクション"
"settings" = "設定"
"accountInfo" = "アカウント情報"
@@ -426,6 +465,14 @@
"enableDesc" = "組み込みDNSサーバーを有効にする"
"tag" = "DNSインバウンドタグ"
"tagDesc" = "このタグはルーティングルールでインバウンドタグとして使用できます"
"clientIp" = "クライアントIP"
"clientIpDesc" = "DNSクエリ中に指定されたIPの位置をサーバーに通知するために使用されます"
"disableCache" = "キャッシュを無効にする"
"disableCacheDesc" = "DNSキャッシュを無効にします"
"disableFallback" = "フォールバックを無効にする"
"disableFallbackDesc" = "フォールバックDNSクエリを無効にします"
"disableFallbackIfMatch" = "一致した場合にフォールバックを無効にする"
"disableFallbackIfMatchDesc" = "DNSサーバーの一致するドメインリストにヒットした場合、フォールバックDNSクエリを無効にします"
"strategy" = "クエリ戦略"
"strategyDesc" = "ドメイン名解決の全体的な戦略"
"add" = "サーバー追加"
@@ -440,7 +487,7 @@
"poolSize" = "プールサイズ"
[pages.settings.security]
"admin" = "管理者"
"admin" = "管理者の資格情報"
"secret" = "セキュリティトークン"
"loginSecurity" = "ログインセキュリティ"
"loginSecurityDesc" = "追加の認証を追加してセキュリティを向上させる"
@@ -483,9 +530,9 @@
"status" = "✅ ボットは正常に動作しています!"
"usage" = "❗ 検索するテキストを入力してください!"
"getID" = "🆔 あなたのIDは<code>{{ .ID }}</code>"
"helpAdminCommands" = "Xray Coreを再起動するには\r\n<code>/restart force</code>\r\n\r\nクライアントの電子メールを検索するには\r\n<code>/usage [電子メール]</code>\r\n\r\nインバウンドクライアントの統計情報を含むを検索するには\r\n<code>/inbound [備考]</code>\r\n\r\nTelegramチャットID\r\n<code>/id</code>"
"helpAdminCommands" = "Xray Coreを再起動するには\r\n<code>/restart</code>\r\n\r\nクライアントの電子メールを検索するには\r\n<code>/usage [電子メール]</code>\r\n\r\nインバウンドクライアントの統計情報を含むを検索するには\r\n<code>/inbound [備考]</code>\r\n\r\nTelegramチャットID\r\n<code>/id</code>"
"helpClientCommands" = "統計情報を検索するには、次のコマンドを使用してください:\r\n<code>/usage [電子メール]</code>\r\n\r\nTelegramチャットID\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart force</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ 操作成功!"
"restartFailed" = "❗ 操作エラー。\r\n\r\n<code>エラー: {{ .Error }}</code>"
"xrayNotRunning" = "❗ Xray Core は動作していません。"

View File

@@ -44,6 +44,7 @@
"monitor" = "IP de Escuta"
"certificate" = "Certificado Digital"
"fail" = "Falhou"
"comment" = "Comentário"
"success" = "Com Sucesso"
"getVersion" = "Obter Versão"
"install" = "Instalar"
@@ -60,8 +61,15 @@
"secAlertPanelURI" = "O caminho URI padrão do painel não é seguro. Configure um caminho URI complexo."
"secAlertSubURI" = "O caminho URI padrão de inscrição não é seguro. Configure um caminho URI complexo."
"secAlertSubJsonURI" = "O caminho URI JSON de inscrição padrão não é seguro. Configure um caminho URI complexo."
"emptyDnsDesc" = "Nenhum servidor DNS adicionado."
"emptyFakeDnsDesc" = "Nenhum servidor Fake DNS adicionado."
"emptyBalancersDesc" = "Nenhum balanceador adicionado."
"emptyReverseDesc" = "Nenhum proxy reverso adicionado."
[menu]
"theme" = "Tema"
"dark" = "Escuro"
"ultraDark" = "Ultra Escuro"
"dashboard" = "Visão Geral"
"inbounds" = "Inbounds"
"settings" = "Panel Settings"
@@ -97,20 +105,25 @@
"connectionTcpCountDesc" = "Total de conexões TCP no sistema"
"connectionUdpCountDesc" = "Total de conexões UDP no sistema"
"connectionCount" = "Estatísticas de Conexão"
"upSpeed" = "Velocidade total de upload no sistema"
"downSpeed" = "Velocidade total de download no sistema"
"totalSent" = "Dados totais enviados desde a inicialização do sistema"
"totalReceive" = "Dados totais recebidos desde a inicialização do sistema"
"ipAddresses" = "Endereços IP"
"toggleIpVisibility" = "Alternar visibilidade do IP"
"overallSpeed" = "Velocidade geral"
"upload" = "Upload"
"download" = "Download"
"totalData" = "Dados totais"
"sent" = "Enviado"
"received" = "Recebido"
"xraySwitchVersionDialog" = "Alterar Versão do Xray"
"xraySwitchVersionDialogDesc" = "Tem certeza de que deseja alterar a versão do Xray para"
"dontRefresh" = "Instalação em andamento, por favor não atualize a página"
"logs" = "Logs"
"config" = "Configuração"
"backup" = "Backup e Restauração"
"backup" = "Backup"
"backupTitle" = "Backup e Restauração do Banco de Dados"
"backupDescription" = "É recomendado fazer um backup antes de restaurar o banco de dados."
"exportDatabase" = "Fazer Backup"
"exportDatabase" = "Backup"
"exportDatabaseDesc" = "Clique para baixar um arquivo .db contendo um backup do seu banco de dados atual para o seu dispositivo."
"importDatabase" = "Restaurar"
"importDatabaseDesc" = "Clique para selecionar e enviar um arquivo .db do seu dispositivo para restaurar seu banco de dados a partir de um backup."
[pages.inbounds]
"title" = "Inbounds"
@@ -129,6 +142,8 @@
"resetTraffic" = "Redefinir Tráfego"
"addInbound" = "Adicionar Inbound"
"generalActions" = "Ações Gerais"
"autoRefresh" = "Atualização automática"
"autoRefreshInterval" = "Intervalo"
"create" = "Criar"
"update" = "Atualizar"
"modifyInbound" = "Modificar Inbound"
@@ -286,6 +301,8 @@
"subSettings" = "Assinatura"
"subEnable" = "Ativar Serviço de Assinatura"
"subEnableDesc" = "Ativa o serviço de assinatura."
"subTitle" = "Título da Assinatura"
"subTitleDesc" = "Título exibido no cliente VPN"
"subListen" = "IP de Escuta"
"subListenDesc" = "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)"
"subPort" = "Porta de Escuta"
@@ -306,6 +323,10 @@
"subShowInfoDesc" = "O tráfego restante e a data serão exibidos nos aplicativos de cliente."
"subURI" = "URI de Proxy Reverso"
"subURIDesc" = "O caminho URI da URL de assinatura para uso por trás de proxies."
"externalTrafficInformEnable" = "Informações de tráfego externo"
"externalTrafficInformEnableDesc" = "Informar a API externa sobre cada atualização de tráfego."
"externalTrafficInformURI" = "URI de informação de tráfego externo"
"externalTrafficInformURIDesc" = "As atualizações de tráfego são enviadas para este URI."
"fragment" = "Fragmentação"
"fragmentDesc" = "Ativa a fragmentação para o pacote TLS hello."
"fragmentSett" = "Configurações de Fragmentação"
@@ -316,7 +337,15 @@
"muxSett" = "Configurações de Mux"
"direct" = "Conexão Direta"
"directDesc" = "Estabelece conexões diretamente com domínios ou intervalos de IP de um país específico."
"notifications" = "Notificações"
"certs" = "Certificados"
"externalTraffic" = "Tráfego Externo"
"dateAndTime" = "Data e Hora"
"proxyAndServer" = "Proxy e Servidor"
"intervals" = "Intervalos"
"information" = "Informação"
"language" = "Idioma"
"telegramBotLanguage" = "Idioma do Bot do Telegram"
[pages.xray]
"title" = "Configurações Xray"
@@ -369,6 +398,15 @@
"dnsLogDesc" = "Se ativar logs de consulta DNS"
"maskAddress" = "Mascarar Endereço"
"maskAddressDesc" = "Máscara de endereço IP, quando ativado, substitui automaticamente o endereço IP que aparece no log."
"statistics" = "Estatísticas"
"statsInboundUplink" = "Estatísticas de Upload de Entrada"
"statsInboundUplinkDesc" = "Habilita a coleta de estatísticas para o tráfego de upload de todos os proxies de entrada."
"statsInboundDownlink" = "Estatísticas de Download de Entrada"
"statsInboundDownlinkDesc" = "Habilita a coleta de estatísticas para o tráfego de download de todos os proxies de entrada."
"statsOutboundUplink" = "Estatísticas de Upload de Saída"
"statsOutboundUplinkDesc" = "Habilita a coleta de estatísticas para o tráfego de upload de todos os proxies de saída."
"statsOutboundDownlink" = "Estatísticas de Download de Saída"
"statsOutboundDownlinkDesc" = "Habilita a coleta de estatísticas para o tráfego de download de todos os proxies de saída."
[pages.xray.rules]
"first" = "Primeiro"
@@ -398,6 +436,7 @@
"type" = "Tipo"
"bridge" = "Ponte"
"portal" = "Portal"
"link" = "Link"
"intercon" = "Interconexão"
"settings" = "Configurações"
"accountInfo" = "Informações da Conta"
@@ -426,6 +465,14 @@
"enableDesc" = "Ativar o servidor DNS integrado"
"tag" = "Tag de Entrada DNS"
"tagDesc" = "Esta tag estará disponível como uma tag de Entrada nas regras de roteamento."
"clientIp" = "IP do Cliente"
"clientIpDesc" = "Usado para notificar o servidor sobre a localização IP especificada durante consultas DNS"
"disableCache" = "Desativar cache"
"disableCacheDesc" = "Desativa o cache de DNS"
"disableFallback" = "Desativar Fallback"
"disableFallbackDesc" = "Desativa consultas DNS de fallback"
"disableFallbackIfMatch" = "Desativar Fallback Se Corresponder"
"disableFallbackIfMatchDesc" = "Desativa consultas DNS de fallback quando a lista de domínios correspondentes do servidor DNS é atingida"
"strategy" = "Estratégia de Consulta"
"strategyDesc" = "Estratégia geral para resolver nomes de domínio"
"add" = "Adicionar Servidor"
@@ -440,7 +487,7 @@
"poolSize" = "Tamanho do Pool"
[pages.settings.security]
"admin" = "Admin"
"admin" = "Credenciais de administrador"
"secret" = "Token Secreto"
"loginSecurity" = "Login Seguro"
"loginSecurityDesc" = "Adiciona uma camada extra de autenticação para fornecer mais segurança."
@@ -483,9 +530,9 @@
"status" = "✅ Bot está OK!"
"usage" = "❗ Por favor, forneça um texto para pesquisar!"
"getID" = "🆔 Seu ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Para reiniciar o Xray Core:\r\n<code>/restart force</code>\r\n\r\nPara pesquisar por um email de cliente:\r\n<code>/usage [Email]</code>\r\n\r\nPara pesquisar por inbounds (com estatísticas do cliente):\r\n<code>/inbound [Remark]</code>\r\n\r\nTelegram Chat ID:\r\n<code>/id</code>"
"helpAdminCommands" = "Para reiniciar o Xray Core:\r\n<code>/restart</code>\r\n\r\nPara pesquisar por um email de cliente:\r\n<code>/usage [Email]</code>\r\n\r\nPara pesquisar por inbounds (com estatísticas do cliente):\r\n<code>/inbound [Remark]</code>\r\n\r\nTelegram Chat ID:\r\n<code>/id</code>"
"helpClientCommands" = "Para pesquisar por estatísticas, use o seguinte comando:\r\n\r\n<code>/usage [Email]</code>\r\n\r\nTelegram Chat ID:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart force</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Operação bem-sucedida!"
"restartFailed" = "❗ Erro na operação.\r\n\r\n<code>Erro: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core não está em execução."

View File

@@ -41,16 +41,17 @@
"offline" = "Офлайн"
"online" = "Онлайн"
"domainName" = "Домен"
"monitor" = "Слушать IP"
"monitor" = "Мониторинг IP"
"certificate" = "Цифровой сертификат"
"fail" = "Неудачно"
"fail" = "Ошибка"
"comment" = "Комментарий"
"success" = "Успешно"
"getVersion" = "Узнать версию"
"install" = "Установка"
"clients" = "Клиенты"
"usage" = "Использование"
"secretToken" = "Секретный токен"
"remained" = "остались"
"remained" = "Остаток"
"security" = "Безопасность"
"secAlertTitle" = "Предупреждение системы безопасности"
"secAlertSsl" = "Это соединение не защищено. Пожалуйста, воздержитесь от ввода конфиденциальной информации до тех пор, пока не будет активирован TLS для защиты данных"
@@ -60,8 +61,15 @@
"secAlertPanelURI" = "URI-путь по умолчанию панели небезопасен. Пожалуйста, настройте сложный URI-путь."
"secAlertSubURI" = "URI-путь по умолчанию подписки небезопасен. Пожалуйста, настройте сложный URI-путь."
"secAlertSubJsonURI" = "URI-путь по умолчанию для JSON подписки небезопасен. Пожалуйста, настройте сложный URI-путь."
"emptyDnsDesc" = "Нет добавленных DNS-серверов."
"emptyFakeDnsDesc" = "Нет добавленных Fake DNS-серверов."
"emptyBalancersDesc" = "Нет добавленных балансировщиков."
"emptyReverseDesc" = "Нет добавленных обратных прокси."
[menu]
"theme" = "Тема"
"dark" = "Темная"
"ultraDark" = "Ультра темная"
"dashboard" = "Статус системы"
"inbounds" = "Подключения"
"settings" = "Настройки панели"
@@ -78,7 +86,7 @@
"invalidFormData" = "Недопустимый формат данных"
"emptyUsername" = "Введите имя пользователя"
"emptyPassword" = "Введите пароль"
"wrongUsernameOrPassword" = "Неверное имя пользователя или пароль"
"wrongUsernameOrPassword" = "Неверное имя пользователя, пароль или секретный токен."
"successLogin" = "Успешный вход"
[pages.index]
@@ -88,33 +96,38 @@
"xrayStatus" = "Xray"
"stopXray" = "Остановить"
"restartXray" = "Перезапустить"
"xraySwitch" = "Версия"
"xraySwitch" = "Выбор версии"
"xraySwitchClick" = "Выберите желаемую версию"
"xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями"
"xraySwitchClickDesk" = "Обратите внимание: старые версии могут не поддерживать текущие настройки"
"operationHours" = "Время работы системы"
"systemLoad" = "Системная нагрузка"
"systemLoad" = "Нагрузка на систему"
"systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут"
"connectionTcpCountDesc" = "Общее количество подключений TCP по всем сетевым картам."
"connectionUdpCountDesc" = "Общее количество подключений UDP по всем сетевым картам."
"connectionCount" = "Количество соединений"
"upSpeed" = "Общая скорость upload для всех сетей"
"downSpeed" = "Общая скорость download для всех сетей"
"totalSent" = "Общий объем загруженных данных для всех сетей с момента запуска системы"
"totalReceive" = "Общий объем полученных данных для всех сетей с момента запуска системы."
"ipAddresses" = "IP-адреса"
"toggleIpVisibility" = "Переключить видимость IP"
"overallSpeed" = "Общая скорость"
"upload" = "Отправка"
"download" = "Загрузка"
"totalData" = "Общий объем данных"
"sent" = "Отправлено"
"received" = "Получено"
"xraySwitchVersionDialog" = "Переключить версию Xray"
"xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?"
"dontRefresh" = "Идёт установка. Пожалуйста, не обновляйте эту страницу"
"dontRefresh" = "Установка в процессе. Не обновляйте страницу"
"logs" = "Логи"
"config" = "Конфигурация"
"backup" = "Бэкап и восстановление"
"backupTitle" = "База данных бэкапа и восстановления"
"backupDescription" = "Не забудьте сделать резервную копию перед импортом новой базы данных"
"backup" = "Резервная копия"
"backupTitle" = "База данных резервных копий"
"exportDatabase" = "Экспорт базы данных"
"exportDatabaseDesc" = "Нажмите, чтобы скачать файл .db, содержащий резервную копию вашей текущей базы данных на ваше устройство."
"importDatabase" = "Импорт базы данных"
"importDatabaseDesc" = "Нажмите, чтобы выбрать и загрузить файл .db с вашего устройства для восстановления базы данных из резервной копии."
[pages.inbounds]
"title" = "Подключения"
"totalDownUp" = "Всего uploads/downloads"
"totalDownUp" = "Общий объем отправленного/полученного трафика"
"totalUsage" = "Всего использовано"
"inboundCount" = "Количество подключений"
"operate" = "Меню"
@@ -129,6 +142,8 @@
"resetTraffic" = "Сбросить трафик"
"addInbound" = "Добавить подключение"
"generalActions" = "Общие действия"
"autoRefresh" = "Автообновление"
"autoRefreshInterval" = "Интервал"
"create" = "Создать"
"update" = "Обновить"
"modifyInbound" = "Изменить подключение"
@@ -146,17 +161,17 @@
"meansNoLimit" = "= Без ограничений (значение: ГБ)"
"totalFlow" = "Общий расход"
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы не истекало"
"noRecommendKeepDefault" = "Не рекомендуется оставлять настройки по умолчанию"
"noRecommendKeepDefault" = "Рекомендуется оставить настройки по умолчанию"
"certificatePath" = "Путь к файлу"
"certificateContent" = "Содержимое файла"
"publicKey" = "Публичный ключ"
"privatekey" = "Приватный ключ"
"privatekey" = "Закрытый ключ"
"clickOnQRcode" = "Нажмите на QR-код, чтобы скопировать"
"client" = "Клиент"
"export" = "Экспорт ключей"
"clone" = "Клонировать"
"cloneInbound" = "Клонировать"
"cloneInboundContent" = "Все настройки этого подключения, кроме порта, IP-адреса прослушки и клиентов, будут клонированы"
"cloneInboundContent" = "Будут клонированы все настройки подключений, за исключением списка клиентов, порта и IP-адреса прослушивания"
"cloneInboundOk" = "Клонировано"
"resetAllTraffic" = "Сбросить трафик всех подключений"
"resetAllTrafficTitle" = "Сброс трафика всех подключений"
@@ -172,9 +187,9 @@
"delDepletedClientsContent" = "Вы уверены, что хотите удалить всех отключенных пользователей?"
"email" = "Email"
"emailDesc" = "Пожалуйста, укажите уникальный Email"
"IPLimit" = "Ограничение по IP"
"IPLimitDesc" = "Сбросить подключение, если подключено больше введенного значения (введите 0, чтобы отключить ограничение IP-адресов)"
"IPLimitlog" = "IP лог"
"IPLimit" = "Лимит по IP"
"IPLimitDesc" = "Ограничение количества подключений с одного IP (0 отключить)"
"IPLimitlog" = "Лог IP-адресов"
"IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить список)"
"IPLimitlogclear" = "Очистить лог"
"setDefaultCert" = "Установить сертификат с панели"
@@ -228,37 +243,37 @@
"save" = "Сохранить"
"infoDesc" = "Каждое выполненное изменение необходимо сохранить. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу"
"restartPanel" = "Перезапуск панели"
"restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Нажмите ОК для перезапуска панели через 3 сек. Если вы не можете пользоваться панелью после перезапуска, пожалуйста, посмотрите лог панели на сервере"
"restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель станет недоступной, проверьте лог сервера"
"actions" = "Действия"
"resetDefaultConfig" = "Сбросить на конфигурацию по умолчанию"
"resetDefaultConfig" = "Восстановить настройки по умолчанию"
"panelSettings" = "Настройки панели"
"securitySettings" = "Настройки безопасности"
"TGBotSettings" = "Настройки Telegram бота"
"panelListeningIP" = "IP-адрес панели"
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
"panelListeningDomain" = "Домен прослушивания панели"
"panelListeningDomain" = "Домен панели"
"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы отслеживать все домены и IP-адреса"
"panelPort" = "Порт панели"
"panelPortDesc" = "Порт, используемый для отображения этой панели"
"panelPortDesc" = "Порт, на котором работает панель"
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
"publicKeyPathDesc" = "Введите полный путь, начинающийся с '/'"
"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели"
"privateKeyPathDesc" = "Введите полный путь, начинающийся с '/'"
"panelUrlPath" = "Корневой путь URL адреса панели"
"panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться на '/'"
"panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'"
"pageSize" = "Размер нумерации страниц"
"pageSizeDesc" = "Определить размер страницы для входящей таблицы. Установите 0, чтобы отключить"
"remarkModel" = "Модель примечания и символ разделения"
"datepicker" = "Выбор даты"
"datepickerPlaceholder" = "Выберите дату"
"datepickerDescription" = "Запланированные задачи выполняются в соответствии с данным календарём"
"datepickerDescription" = "Запланированные задачи будут выполняться в выбранное время"
"sampleRemark" = "Пример замечания"
"oldUsername" = "Текущий логин"
"currentPassword" = "Текущий пароль"
"newUsername" = "Новый логин"
"newPassword" = "Новый пароль"
"telegramBotEnable" = "Включить Telegram бота"
"telegramBotEnableDesc" = "Подключайтесь к функциям этой панели через Telegram бота"
"telegramBotEnableDesc" = "Получайте доступ к функциям панели через Telegram-бота"
"telegramToken" = "Токен Telegram бота"
"telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
"telegramProxy" = "Прокси Socks5"
@@ -266,16 +281,16 @@
"telegramAPIServer" = "API-сервер Telegram"
"telegramAPIServerDesc" = "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию."
"telegramChatId" = "Идентификатор Telegram администратора бота"
"telegramChatIdDesc" = "Один или несколько идентификаторов администратора бота. Чтобы получить идентификатор, используйте @userinfobot или команду '/id' в боте."
"telegramChatIdDesc" = "Один или несколько идентификаторов администратора бота. Для получения идентификатора используйте @userinfobot или команду '/id' в боте."
"telegramNotifyTime" = "Частота уведомлений бота Telegram"
"telegramNotifyTimeDesc" = "Используйте формат времени Crontab"
"telegramNotifyTimeDesc" = "Укажите интервал уведомлений в формате Crontab"
"tgNotifyBackup" = "Резервное копирование базы данных"
"tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете"
"tgNotifyBackupDesc" = "Отправлять уведомление с файлом резервной копии базы данных"
"tgNotifyLogin" = "Уведомление о входе"
"tgNotifyLoginDesc" = "Отображает имя пользователя, IP-адрес и время, когда кто-то пытается войти в вашу панель."
"sessionMaxAge" = "Продолжительность сессии"
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)"
"expireTimeDiff" = "Порог истечения срока сессии для уведомления"
"expireTimeDiff" = "Задержка уведомления об истечении сессии"
"expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)"
"trafficDiff" = "Порог трафика для уведомления"
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)"
@@ -284,8 +299,10 @@
"timeZone" = "Часовой пояс"
"timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе"
"subSettings" = "Подписка"
"subEnable" = "Включить службу"
"subEnable" = "Включить подписку"
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
"subTitle" = "Заголовок подписки"
"subTitleDesc" = "Название подписки, которое видит клиент в VPN клиенте"
"subListen" = "Прослушивание IP"
"subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса"
"subPort" = "Порт подписки"
@@ -303,20 +320,32 @@
"subEncrypt" = "Шифровать конфиги"
"subEncryptDesc" = "Шифровать возвращенные конфиги в подписке"
"subShowInfo" = "Показать информацию об использовании"
"subShowInfoDesc" = "Показывать оставшиеся трафик и дату после имени конфигурации"
"subShowInfoDesc" = "Отображать остаток трафика и дату окончания после имени конфигурации"
"subURI" = "URI обратного прокси"
"subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами"
"externalTrafficInformEnable" = "Информация о внешнем трафике"
"externalTrafficInformEnableDesc" = "Информировать внешний API о каждом обновлении трафика"
"externalTrafficInformURI" = "URI информации о внешнем трафике"
"externalTrafficInformURIDesc" = "Обновления трафика отправляются на этот URI"
"fragment" = "Фрагментация"
"fragmentDesc" = "Включить фрагментацию для пакета приветствия TLS"
"fragmentDesc" = "Включить фрагментацию TLS-приветствия"
"fragmentSett" = "Настройки фрагментации"
"noisesDesc" = "Включить Noises."
"noisesSett" = "Настройки Noises"
"mux" = "Mux"
"muxDesc" = "Передача нескольких независимых потоков данных в рамках установленного потока данных."
"muxDesc" = "Передача нескольких независимых потоков данных в одном соединении."
"muxSett" = "Mux Настройки"
"direct" = "Прямая связь"
"directDesc" = "Напрямую устанавливает соединения с доменами или диапазонами IP конкретной страны."
"directDesc" = "Устанавливает прямые соединения с доменами или IP-адресами определённой страны."
"notifications" = "Уведомления"
"certs" = "Сертификаты"
"externalTraffic" = "Внешний трафик"
"dateAndTime" = "Дата и время"
"proxyAndServer" = "Прокси и сервер"
"intervals" = "Интервалы"
"information" = "Информация"
"language" = "Язык"
"telegramBotLanguage" = "Язык Telegram-бота"
[pages.xray]
"title" = "Настройки Xray"
@@ -327,15 +356,15 @@
"generalConfigs" = "Основные настройки"
"generalConfigsDesc" = "Эти параметры описывают общие настройки"
"logConfigs" = "Журнал"
"logConfigsDesc" = "Журналы могут повлиять на эффективность вашего сервера. Рекомендуется включать их только в случае необходимости!"
"logConfigsDesc" = "Логи могут замедлять работу сервера. Включайте только нужные вам виды логов при необходимости!"
"blockConfigs" = "Блокировка конфигураций"
"blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
"basicRouting" = "Базовые соединения"
"blockConnectionsConfigsDesc" = "Эти параметры будут блокировать трафик в зависимости от запрашиваемой страны."
"directConnectionsConfigsDesc" = "Прямое соединение гарантирует, что определенный трафик не будет перенаправлен через другой сервер."
"blockips" = "Блокировать IP"
"blockips" = "Блокировать IP-адреса"
"blockdomains" = "Блокировать домены"
"directips" = "Прямые IP"
"directips" = "Прямые IP-адреса"
"directdomains" = "Прямые домены"
"ipv4Routing" = "Правила IPv4"
"ipv4RoutingDesc" = "Эти параметры позволят пользователям маршрутизироваться к целевым доменам только через IPv4"
@@ -368,7 +397,16 @@
"dnsLog" = "DNS Журнал"
"dnsLogDesc" = "Включить логи запросов DNS"
"maskAddress" = "Маскировать Адрес"
"maskAddressDesc" = "Маска IP-адреса, при активации автоматически заменяет IP-адрес, который появляется в логе."
"maskAddressDesc" = "При активации реальный IP-адрес заменяется на маскировочный в логах."
"statistics" = "Статистика"
"statsInboundUplink" = "Статистика входящего аплинка"
"statsInboundUplinkDesc" = "Включает сбор статистики для исходящего трафика всех входящих прокси."
"statsInboundDownlink" = "Статистика входящего даунлинка"
"statsInboundDownlinkDesc" = "Включает сбор статистики для входящего трафика всех входящих прокси."
"statsOutboundUplink" = "Статистика исходящего аплинка"
"statsOutboundUplinkDesc" = "Включает сбор статистики для исходящего трафика всех исходящих прокси."
"statsOutboundDownlink" = "Статистика исходящего даунлинка"
"statsOutboundDownlinkDesc" = "Включает сбор статистики для входящего трафика всех исходящих прокси."
[pages.xray.rules]
"first" = "Первый"
@@ -383,7 +421,7 @@
"info" = "Информация"
"add" = "Добавить правило"
"edit" = "Редактировать правило"
"useComma" = "Элементы, разделенные запятыми"
"useComma" = "Элементы, разделённые запятыми"
[pages.xray.outbound]
"addOutbound" = "Добавить исходящий"
@@ -398,6 +436,7 @@
"type" = "Тип"
"bridge" = "Мост"
"portal" = "Портал"
"link" = "Ссылка"
"intercon" = "Соединение"
"settings" = "Настройки"
"accountInfo" = "Информация об учетной записи"
@@ -414,7 +453,7 @@
"balancerDesc" = "Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag."
[pages.xray.wireguard]
"secretKey" = "Приватный ключ"
"secretKey" = "Закрытый ключ"
"publicKey" = "Публичный ключ"
"allowedIPs" = "Разрешенные IP-адреса"
"endpoint" = "Конечная точка"
@@ -426,6 +465,14 @@
"enableDesc" = "Включить встроенный DNS-сервер"
"tag" = "Входящий тег DNS"
"tagDesc" = "Этот тег будет доступен как входящий тег в правилах маршрутизации."
"clientIp" = "IP клиента"
"clientIpDesc" = "Используется для уведомления сервера о указанном местоположении IP во время DNS-запросов"
"disableCache" = "Отключить кэш"
"disableCacheDesc" = "Отключает кэширование DNS"
"disableFallback" = "Отключить резервный DNS"
"disableFallbackDesc" = "Отключает резервные DNS-запросы"
"disableFallbackIfMatch" = "Отключить резервный DNS при совпадении"
"disableFallbackIfMatchDesc" = "Отключает резервные DNS-запросы при совпадении списка доменов DNS-сервера"
"strategy" = "Стратегия запроса"
"strategyDesc" = "Общая стратегия разрешения доменных имен"
"add" = "Добавить сервер"
@@ -440,7 +487,7 @@
"poolSize" = "Размер пула"
[pages.settings.security]
"admin" = "Админ"
"admin" = "Учетные данные администратора"
"secret" = "Секретный токен"
"loginSecurity" = "Безопасность входа"
"loginSecurityDesc" = "Включить дополнительные меры безопасности входа пользователя"
@@ -448,7 +495,7 @@
"secretTokenDesc" = "Пожалуйста, скопируйте и сохраните этот токен в безопасном месте. Этот токен необходим для входа в систему и не может быть восстановлен с помощью инструмента x-ui"
[pages.settings.toasts]
"modifySettings" = "Изменение настроек"
"modifySettings" = "Настройки изменены"
"getSettings" = "Просмотр настроек"
"modifyUser" = "Изменение пользователя"
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
@@ -483,9 +530,9 @@
"status" = "✅ Бот работает нормально!"
"usage" = "❗ Пожалуйста, укажите текст для поиска!"
"getID" = "🆔 Ваш ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Для перезапуска Xray Core:\r\n<code>/restart force</code>\r\n\r\nДля поиска электронной почты клиента:\r\n<code>/usage [Email]</code>\r\n\r\nДля поиска входящих (со статистикой клиента):\r\n<code>/inbound [Примечание]</code>\r\n\r\nID чата Telegram:\r\n<code>/id</code>"
"helpClientCommands" = "Для поиска статистики используйте следующую команду:\r\n<code>/usage [Email]</code>\r\n\r\nID чата Telegram:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart force</code>"
"helpAdminCommands" = "Для перезапуска Xray Core:\r\n<code>/restart</code>\r\n\r\nДля поиска электронной почты клиента:\r\n<code>/usage [Email]</code>\r\n\r\nДля поиска входящих (со статистикой клиента):\r\n<code>/inbound [Примечание]</code>\r\n\r\nID чата Telegram:\r\n<code>/id</code>"
"helpClientCommands" = "Для поиска статистики используйте команду:\r\n<code>/usage [Email]</code>\r\n\r\nID чата Telegram:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Операция успешно завершена!"
"restartFailed" = "❗ Ошибка в операции.\r\n\r\n<code>Ошибка: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core не запущен."
@@ -572,7 +619,7 @@
"allClients" = "Все клиенты"
[tgbot.answers]
"successfulOperation" = "✅ Успешный!"
"successfulOperation" = "✅ Успешно!"
"errorOperation" = "❗ Ошибка в операции."
"getInboundsFailed" = "❌ Не удалось получить входящие потоки."
"getClientsFailed" = "❌ Не удалось получить клиентов."
@@ -581,8 +628,8 @@
"IpRefreshSuccess" = "✅ {{ .Email }}: IP-адреса успешно обновлены."
"TGIdRefreshSuccess" = "✅ {{ .Email }}: Пользователь Telegram клиента успешно обновлен."
"resetTrafficSuccess" = "✅ {{ .Email }}: Трафик успешно сброшен."
"setTrafficLimitSuccess" = "✅ {{ .Email }}: Лимит Трафик успешно сохранен."
"expireResetSuccess" = "✅ {{ .Email }}: Дни истечения успешно сброшены."
"setTrafficLimitSuccess" = "✅ {{ .Email }}: Лимит трафика успешно установлен."
"expireResetSuccess" = "✅ {{ .Email }}: Срок действия успешно сброшен."
"resetIpSuccess" = "✅ {{ .Email }}: Лимит IP ({{ .Count }}) успешно сохранен."
"clearIpSuccess" = "✅ {{ .Email }}: IP-адреса успешно очищены."
"getIpLog" = "✅ {{ .Email }}: Получен лог IP."

View File

@@ -44,6 +44,7 @@
"monitor" = "Dinleme IP"
"certificate" = "Dijital Sertifika"
"fail" = "Başarısız"
"comment" = "Yorum"
"success" = "Başarılı"
"getVersion" = "Sürümü Al"
"install" = "Yükle"
@@ -60,8 +61,15 @@
"secAlertPanelURI" = "Panel varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın."
"secAlertSubURI" = "Abonelik varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın."
"secAlertSubJsonURI" = "Abonelik JSON varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın."
"emptyDnsDesc" = "Eklenmiş DNS sunucusu yok."
"emptyFakeDnsDesc" = "Eklenmiş Fake DNS sunucusu yok."
"emptyBalancersDesc" = "Eklenmiş dengeleyici yok."
"emptyReverseDesc" = "Eklenmiş ters proxy yok."
[menu]
"theme" = "Tema"
"dark" = "Koyu"
"ultraDark" = "Ultra Koyu"
"dashboard" = "Genel Bakış"
"inbounds" = "Gelenler"
"settings" = "Panel Ayarları"
@@ -97,20 +105,25 @@
"connectionTcpCountDesc" = "Sistem genelinde toplam TCP bağlantıları"
"connectionUdpCountDesc" = "Sistem genelinde toplam UDP bağlantıları"
"connectionCount" = "Bağlantı İstatistikleri"
"upSpeed" = "Sistem genelinde toplam yükleme hızı"
"downSpeed" = "Sistem genelinde toplam indirme hızı"
"totalSent" = "İşletim sistemi başlatıldığından beri sistem genelinde gönderilen toplam veri"
"totalReceive" = "İşletim sistemi başlatıldığından beri sistem genelinde alınan toplam veri"
"ipAddresses" = "IP adresleri"
"toggleIpVisibility" = "IP görünürlüğünü değiştir"
"overallSpeed" = "Genel hız"
"upload" = "Yükleme"
"download" = "İndirme"
"totalData" = "Toplam veri"
"sent" = "Gönderilen"
"received" = "Alınan"
"xraySwitchVersionDialog" = "Xray Sürümünü Değiştir"
"xraySwitchVersionDialogDesc" = "Xray sürümünü değiştirmek istediğinizden emin misiniz"
"dontRefresh" = "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin"
"logs" = "Günlükler"
"config" = "Yapılandırma"
"backup" = "Yedekle & Geri Yükle"
"backup" = "Yedek"
"backupTitle" = "Veritabanı Yedekleme & Geri Yükleme"
"backupDescription" = "Veritabanını geri yüklemeden önce yedek almanız önerilir."
"exportDatabase" = "Yedekle"
"exportDatabaseDesc" = "Mevcut veritabanınızın yedeğini içeren bir .db dosyasını cihazınıza indirmek için tıklayın."
"importDatabase" = "Geri Yükle"
"importDatabaseDesc" = "Cihazınızdan bir .db dosyası seçip yükleyerek veritabanınızı yedekten geri yüklemek için tıklayın."
[pages.inbounds]
"title" = "Gelenler"
@@ -129,6 +142,8 @@
"resetTraffic" = "Trafiği Sıfırla"
"addInbound" = "Gelen Ekle"
"generalActions" = "Genel Eylemler"
"autoRefresh" = "Otomatik yenileme"
"autoRefreshInterval" = "Aralık"
"create" = "Oluştur"
"update" = "Güncelle"
"modifyInbound" = "Geleni Düzenle"
@@ -286,6 +301,8 @@
"subSettings" = "Abonelik"
"subEnable" = "Abonelik Hizmetini Etkinleştir"
"subEnableDesc" = "Abonelik hizmetini etkinleştirir."
"subTitle" = "Abonelik Başlığı"
"subTitleDesc" = "VPN istemcisinde gösterilen başlık"
"subListen" = "Dinleme IP"
"subListenDesc" = "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)"
"subPort" = "Dinleme Portu"
@@ -306,6 +323,10 @@
"subShowInfoDesc" = "Kalan trafik ve tarih müşteri uygulamalarında görüntülenir."
"subURI" = "Ters Proxy URI"
"subURIDesc" = "Proxy arkasında kullanılacak abonelik URL'sinin URI yolu."
"externalTrafficInformEnable" = "Harici Trafik Bilgisi"
"externalTrafficInformEnableDesc" = "Her trafik güncellemesinde harici API'yi bilgilendirin."
"externalTrafficInformURI" = "Harici Trafik Bilgisi URI'si"
"externalTrafficInformURIDesc" = "Trafik güncellemeleri bu URI'ye gönderildi."
"fragment" = "Parçalama"
"fragmentDesc" = "TLS merhaba paketinin parçalanmasını etkinleştir."
"fragmentSett" = "Parçalama Ayarları"
@@ -316,7 +337,15 @@
"muxSett" = "Mux Ayarları"
"direct" = "Doğrudan Bağlantı"
"directDesc" = "Belirli bir ülkenin alan adları veya IP aralıkları ile doğrudan bağlantı kurar."
"notifications" = "Bildirimler"
"certs" = "Sertifikalar"
"externalTraffic" = "Harici Trafik"
"dateAndTime" = "Tarih ve Saat"
"proxyAndServer" = "Proxy ve Sunucu"
"intervals" = "Aralıklar"
"information" = "Bilgi"
"language" = "Dil"
"telegramBotLanguage" = "Telegram Bot Dili"
[pages.xray]
"title" = "Xray Yapılandırmaları"
@@ -369,6 +398,15 @@
"dnsLogDesc" = "DNS sorgu günlüklerini etkinleştirin"
"maskAddress" = "Adres Maskesi"
"maskAddressDesc" = "IP adresi maskesi, etkinleştirildiğinde, günlükte görünen IP adresini otomatik olarak değiştirecektir."
"statistics" = "İstatistikler"
"statsInboundUplink" = "Gelen Yükleme İstatistikleri"
"statsInboundUplinkDesc" = "Tüm gelen proxy'lerin yükleme trafiği için istatistik toplamayı etkinleştirir."
"statsInboundDownlink" = "Gelen İndirme İstatistikleri"
"statsInboundDownlinkDesc" = "Tüm gelen proxy'lerin indirme trafiği için istatistik toplamayı etkinleştirir."
"statsOutboundUplink" = "Giden Yükleme İstatistikleri"
"statsOutboundUplinkDesc" = "Tüm giden proxy'lerin yükleme trafiği için istatistik toplamayı etkinleştirir."
"statsOutboundDownlink" = "Giden İndirme İstatistikleri"
"statsOutboundDownlinkDesc" = "Tüm giden proxy'lerin indirme trafiği için istatistik toplamayı etkinleştirir."
[pages.xray.rules]
"first" = "İlk"
@@ -398,6 +436,7 @@
"type" = "Tür"
"bridge" = "Köprü"
"portal" = "Portal"
"link" = "Bağlantı"
"intercon" = "Bağlantı"
"settings" = "Ayarlar"
"accountInfo" = "Hesap Bilgileri"
@@ -426,6 +465,14 @@
"enableDesc" = "Dahili DNS sunucusunu etkinleştir"
"tag" = "DNS Gelen Etiketi"
"tagDesc" = "Bu etiket, yönlendirme kurallarında Gelen etiketi olarak kullanılabilir."
"clientIp" = "İstemci IP"
"clientIpDesc" = "DNS sorguları sırasında belirtilen IP konumunu sunucuya bildirmek için kullanılır"
"disableCache" = "Önbelleği devre dışı bırak"
"disableCacheDesc" = "DNS önbelleğini devre dışı bırakır"
"disableFallback" = "Yedeklemeyi devre dışı bırak"
"disableFallbackDesc" = "Yedek DNS sorgularını devre dışı bırakır"
"disableFallbackIfMatch" = "Eşleşirse Yedeklemeyi Devre Dışı Bırak"
"disableFallbackIfMatchDesc" = "DNS sunucusunun eşleşen alan adı listesi vurulduğunda yedek DNS sorgularını devre dışı bırakır"
"strategy" = "Sorgu Stratejisi"
"strategyDesc" = "Alan adlarını çözmek için genel strateji"
"add" = "Sunucu Ekle"
@@ -440,7 +487,7 @@
"poolSize" = "Havuz Boyutu"
[pages.settings.security]
"admin" = "Yönetici"
"admin" = "Yönetici kimlik bilgileri"
"secret" = "Gizli Anahtar"
"loginSecurity" = "Güvenli Giriş"
"loginSecurityDesc" = "Daha fazla güvenlik sağlamak için ek bir kimlik doğrulama katmanı ekler."
@@ -483,9 +530,9 @@
"status" = "✅ Bot çalışıyor!"
"usage" = "❗ Lütfen aramak için bir metin sağlayın!"
"getID" = "🆔 Kimliğiniz: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Xray Core'u yeniden başlatmak için:\r\n<code>/restart force</code>\r\n\r\nBir müşteri e-postasını aramak için:\r\n<code>/usage [E-posta]</code>\r\n\r\nGelenleri aramak için (müşteri istatistikleri ile):\r\n<code>/inbound [Açıklama]</code>\r\n\r\nTelegram Sohbet Kimliği:\r\n<code>/id</code>"
"helpAdminCommands" = "Xray Core'u yeniden başlatmak için:\r\n<code>/restart</code>\r\n\r\nBir müşteri e-postasını aramak için:\r\n<code>/usage [E-posta]</code>\r\n\r\nGelenleri aramak için (müşteri istatistikleri ile):\r\n<code>/inbound [Açıklama]</code>\r\n\r\nTelegram Sohbet Kimliği:\r\n<code>/id</code>"
"helpClientCommands" = "İstatistikleri aramak için şu komutu kullanın:\r\n\r\n<code>/usage [E-posta]</code>\r\n\r\nTelegram Sohbet Kimliği:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart force</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ İşlem başarılı!"
"restartFailed" = "❗ İşlem hatası.\r\n\r\n<code>Hata: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core çalışmıyor."

View File

@@ -44,6 +44,7 @@
"monitor" = "Слухати IP"
"certificate" = "Цифровий сертифікат"
"fail" = "Помилка"
"comment" = "Коментар"
"success" = "Успішно"
"getVersion" = "Отримати версію"
"install" = "Встановити"
@@ -60,8 +61,15 @@
"secAlertPanelURI" = "Стандартний URI-шлях панелі небезпечний. Будь ласка, сконфігуруйте складний URI-шлях."
"secAlertSubURI" = "Стандартний URI-шлях підписки небезпечний. Будь ласка, сконфігуруйте складний URI-шлях."
"secAlertSubJsonURI" = "Стандартний URI-шлях JSON підписки небезпечний. Будь ласка, сконфігуруйте складний URI-шлях."
"emptyDnsDesc" = "Немає доданих DNS-серверів."
"emptyFakeDnsDesc" = "Немає доданих Fake DNS-серверів."
"emptyBalancersDesc" = "Немає доданих балансувальників."
"emptyReverseDesc" = "Немає доданих зворотних проксі."
[menu]
"theme" = "Тема"
"dark" = "Темна"
"ultraDark" = "Ультра темна"
"dashboard" = "Огляд"
"inbounds" = "Вхідні"
"settings" = "Параметри панелі"
@@ -97,20 +105,25 @@
"connectionTcpCountDesc" = "Загальна кількість TCP-з'єднань у системі"
"connectionUdpCountDesc" = "Загальна кількість UDP-з'єднань у системі"
"connectionCount" = "Статистика з'єднання"
"upSpeed" = "Загальна швидкість завантаження в системі"
"downSpeed" = "Загальна швидкість завантаження в системі"
"totalSent" = "Загальна кількість даних, надісланих через систему з моменту запуску ОС"
"totalReceive" = "Загальна кількість даних, отриманих системою з моменту запуску ОС"
"ipAddresses" = "IP-адреси"
"toggleIpVisibility" = "Перемкнути видимість IP"
"overallSpeed" = "Загальна швидкість"
"upload" = "Відправка"
"download" = "Завантаження"
"totalData" = "Загальний обсяг даних"
"sent" = "Відправлено"
"received" = "Отримано"
"xraySwitchVersionDialog" = "Змінити версію Xray"
"xraySwitchVersionDialogDesc" = "Ви впевнені, що бажаєте змінити версію Xray на"
"dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку"
"logs" = "Журнали"
"config" = "Конфігурація"
"backup" = "Резервне копіювання та відновлення"
"backup" = "Резервна копія"
"backupTitle" = "Резервне копіювання та відновлення бази даних"
"backupDescription" = "Рекомендується зробити резервну копію перед відновленням бази даних."
"exportDatabase" = "Резервне копіювання"
"exportDatabase" = "Резервна копія"
"exportDatabaseDesc" = "Натисніть, щоб завантажити файл .db, що містить резервну копію вашої поточної бази даних на ваш пристрій."
"importDatabase" = "Відновити"
"importDatabaseDesc" = "Натисніть, щоб вибрати та завантажити файл .db з вашого пристрою для відновлення бази даних з резервної копії."
[pages.inbounds]
"title" = "Вхідні"
@@ -129,6 +142,8 @@
"resetTraffic" = "Скинути трафік"
"addInbound" = "Додати вхідний"
"generalActions" = "Загальні дії"
"autoRefresh" = "Автооновлення"
"autoRefreshInterval" = "Інтервал"
"create" = "Створити"
"update" = "Оновити"
"modifyInbound" = "Змінити вхідний"
@@ -286,6 +301,8 @@
"subSettings" = "Підписка"
"subEnable" = "Увімкнути службу підписки"
"subEnableDesc" = "Вмикає службу підписки."
"subTitle" = "Назва Підписки"
"subTitleDesc" = "Назва, яка відображається у VPN-клієнті"
"subListen" = "Слухати IP"
"subListenDesc" = "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)"
"subPort" = "Слухати порт"
@@ -306,6 +323,10 @@
"subShowInfoDesc" = "Залишок трафіку та дата відображатимуться в клієнтських програмах."
"subURI" = "URI зворотного проксі"
"subURIDesc" = "URI до URL-адреси підписки для використання за проксі."
"externalTrafficInformEnable" = "Інформація про зовнішній трафік"
"externalTrafficInformEnableDesc" = "Інформувати зовнішній API про кожне оновлення трафіку."
"externalTrafficInformURI" = "Інформаційний URI зовнішнього трафіку"
"externalTrafficInformURIDesc" = "Оновлення трафіку надсилаються на цей URI."
"fragment" = "Фрагментація"
"fragmentDesc" = "Увімкнути фрагментацію для пакету привітання TLS"
"fragmentSett" = "Параметри фрагментації"
@@ -316,7 +337,15 @@
"muxSett" = "Налаштування Mux"
"direct" = "Пряме підключення"
"directDesc" = "Безпосередньо встановлює з’єднання з доменами або діапазонами IP певної країни."
"notifications" = "Сповіщення"
"certs" = "Сертифікати"
"externalTraffic" = "Зовнішній трафік"
"dateAndTime" = "Дата та час"
"proxyAndServer" = "Проксі та сервер"
"intervals" = "Інтервали"
"information" = "Інформація"
"language" = "Мова"
"telegramBotLanguage" = "Мова Telegram-бота"
[pages.xray]
"title" = "Xray конфігурації"
@@ -369,6 +398,15 @@
"dnsLogDesc" = "Чи включити журнали запитів DNS"
"maskAddress" = "Маскувати Адресу"
"maskAddressDesc" = "Маска IP-адреси, при активації автоматично замінює IP-адресу, яка з'являється у журналі."
"statistics" = "Статистика"
"statsInboundUplink" = "Статистика вхідного аплінку"
"statsInboundUplinkDesc" = "Увімкнення збору статистики для вхідного трафіку всіх вхідних проксі."
"statsInboundDownlink" = "Статистика вхідного даунлінку"
"statsInboundDownlinkDesc" = "Увімкнення збору статистики для вихідного трафіку всіх вхідних проксі."
"statsOutboundUplink" = "Статистика вихідного аплінку"
"statsOutboundUplinkDesc" = "Увімкнення збору статистики для вхідного трафіку всіх вихідних проксі."
"statsOutboundDownlink" = "Статистика вихідного даунлінку"
"statsOutboundDownlinkDesc" = "Увімкнення збору статистики для вихідного трафіку всіх вихідних проксі."
[pages.xray.rules]
"first" = "Перший"
@@ -398,6 +436,7 @@
"type" = "Тип"
"bridge" = "Міст"
"portal" = "Портал"
"link" = "Посилання"
"intercon" = "Взаємозв'язок"
"settings" = "Налаштування"
"accountInfo" = "Інформація про обліковий запис"
@@ -426,6 +465,14 @@
"enableDesc" = "Увімкнути вбудований DNS-сервер"
"tag" = "Мітка вхідного DNS"
"tagDesc" = "Ця мітка буде доступна як вхідна мітка в правилах маршрутизації."
"clientIp" = "IP клієнта"
"clientIpDesc" = "Використовується для повідомлення серверу про вказане місцезнаходження IP під час DNS-запитів"
"disableCache" = "Вимкнути кеш"
"disableCacheDesc" = "Вимкнути кешування DNS"
"disableFallback" = "Вимкнути резервний DNS"
"disableFallbackDesc" = "Вимкнути резервні DNS-запити"
"disableFallbackIfMatch" = "Вимкнути резервний DNS при збігу"
"disableFallbackIfMatchDesc" = "Вимкнути резервні DNS-запити при збігу списку доменів DNS-сервера"
"strategy" = "Стратегія запиту"
"strategyDesc" = "Загальна стратегія вирішення доменних імен"
"add" = "Додати сервер"
@@ -440,7 +487,7 @@
"poolSize" = "Розмір пулу"
[pages.settings.security]
"admin" = "Адміністратор"
"admin" = "Облікові дані адміністратора"
"secret" = "Секретний маркер"
"loginSecurity" = "Безпечний вхід"
"loginSecurityDesc" = "Додає додатковий рівень автентифікації для забезпечення більшої безпеки."
@@ -483,9 +530,9 @@
"status" = "✅ Бот в порядку!"
"usage" = "❗ Введіть текст для пошуку!"
"getID" = "🆔 Ваш ідентифікатор: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Для перезапуску Xray Core:\r\n<code>/restart force</code>\r\n\r\nДля пошуку електронної пошти клієнта:\r\n<code>/usage [Електронна пошта]</code>\r\n\r\nДля пошуку вхідних (зі статистикою клієнта):\r\n<code>/inbound [Примітка]</code>\r\n\r\nID чату Telegram:\r\n<code>/id</code>"
"helpAdminCommands" = "Для перезапуску Xray Core:\r\n<code>/restart</code>\r\n\r\nДля пошуку електронної пошти клієнта:\r\n<code>/usage [Електронна пошта]</code>\r\n\r\nДля пошуку вхідних (зі статистикою клієнта):\r\n<code>/inbound [Примітка]</code>\r\n\r\nID чату Telegram:\r\n<code>/id</code>"
"helpClientCommands" = "Для пошуку статистики використовуйте наступну команду:\r\n<code>/usage [Електронна пошта]</code>\r\n\r\nID чату Telegram:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart force</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Операція успішна!"
"restartFailed" = "❗ Помилка в операції.\r\n\r\n<code>Помилка: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core не запущений."

View File

@@ -44,6 +44,7 @@
"monitor" = "Listening IP"
"certificate" = "Chứng chỉ số"
"fail" = "Thất bại"
"comment" = "Bình luận"
"success" = "Thành công"
"getVersion" = "Lấy phiên bản"
"install" = "Cài đặt"
@@ -60,8 +61,15 @@
"secAlertPanelURI" = "Đường dẫn URI mặc định của bảng điều khiển không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp."
"secAlertSubURI" = "Đường dẫn URI mặc định của đăng ký không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp."
"secAlertSubJsonURI" = "Đường dẫn URI JSON mặc định của đăng ký không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp."
"emptyDnsDesc" = "Không có máy chủ DNS nào được thêm."
"emptyFakeDnsDesc" = "Không có máy chủ Fake DNS nào được thêm."
"emptyBalancersDesc" = "Không có bộ cân bằng tải nào được thêm."
"emptyReverseDesc" = "Không có proxy ngược nào được thêm."
[menu]
"theme" = "Chủ đề"
"dark" = "Tối"
"ultraDark" = "Siêu tối"
"dashboard" = "Trạng thái hệ thống"
"inbounds" = "Đầu vào khách hàng"
"settings" = "Cài đặt bảng điều khiển"
@@ -97,20 +105,25 @@
"connectionTcpCountDesc" = "Tổng số kết nối TCP trên tất cả các thẻ mạng."
"connectionUdpCountDesc" = "Tổng số kết nối UDP trên tất cả các thẻ mạng."
"connectionCount" = "Số lượng kết nối"
"upSpeed" = "Tổng tốc độ tải lên cho tất cả các thẻ mạng."
"downSpeed" = "Tổng tốc độ tải xuống cho tất cả các thẻ mạng."
"totalSent" = "Tổng lưu lượng tải lên của tất cả các thẻ mạng kể từ khi hệ thống khởi động."
"totalReceive" = "Tổng lưu lượng tải xuống trên tất cả các thẻ mạng kể từ khi hệ thống khởi động."
"ipAddresses" = "Địa chỉ IP"
"toggleIpVisibility" = "Chuyển đổi hiển thị IP"
"overallSpeed" = "Tốc độ tổng thể"
"upload" = "Tải lên"
"download" = "Tải xuống"
"totalData" = "Tổng dữ liệu"
"sent" = "Đã gửi"
"received" = "Đã nhận"
"xraySwitchVersionDialog" = "Chuyển đổi Phiên bản Xray"
"xraySwitchVersionDialogDesc" = "Bạn có chắc chắn muốn chuyển đổi phiên bản Xray sang"
"dontRefresh" = "Đang tiến hành cài đặt, vui lòng không làm mới trang này."
"logs" = "Nhật ký"
"config" = "Cấu hình"
"backup" = "Sao lưu & Khôi phục"
"backup" = "Sao lưu"
"backupTitle" = "Sao lưu & Khôi phục Cơ sở dữ liệu"
"backupDescription" = "Hãy nhớ sao lưu trước khi nhập cơ sở dữ liệu mới."
"exportDatabase" = "Tải về Cơ sở dữ liệu"
"importDatabase" = "Tải lên Cơ sở dữ liệu"
"exportDatabase" = "Sao lưu"
"exportDatabaseDesc" = "Nhấp để tải xuống tệp .db chứa bản sao lưu cơ sở dữ liệu hiện tại của bạn vào thiết bị."
"importDatabase" = "Khôi phục"
"importDatabaseDesc" = "Nhấp để chọn và tải lên tệp .db từ thiết bị của bạn để khôi phục cơ sở dữ liệu từ bản sao lưu."
[pages.inbounds]
"title" = "Điểm vào (Inbounds)"
@@ -129,6 +142,8 @@
"resetTraffic" = "Đặt lại lưu lượng"
"addInbound" = "Thêm điểm vào"
"generalActions" = "Hành động chung"
"autoRefresh" = "Tự động làm mới"
"autoRefreshInterval" = "Khoảng thời gian"
"create" = "Tạo mới"
"update" = "Cập nhật"
"modifyInbound" = "Chỉnh sửa điểm vào (Inbound)"
@@ -286,6 +301,8 @@
"subSettings" = "Gói đăng ký"
"subEnable" = "Bật dịch vụ"
"subEnableDesc" = "Tính năng gói đăng ký với cấu hình riêng"
"subTitle" = "Tiêu đề Đăng ký"
"subTitleDesc" = "Tiêu đề hiển thị trong ứng dụng VPN"
"subListen" = "Listening IP"
"subListenDesc" = "Mặc định để trống để nghe tất cả các IP"
"subPort" = "Cổng gói đăng ký"
@@ -306,6 +323,10 @@
"subShowInfoDesc" = "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình"
"subURI" = "URI proxy trung gian"
"subURIDesc" = "Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian"
"externalTrafficInformEnable" = "Thông báo giao thông bên ngoài"
"externalTrafficInformEnableDesc" = "Thông báo cho API bên ngoài về mọi cập nhật lưu lượng truy cập."
"externalTrafficInformURI" = "URI thông báo lưu lượng truy cập bên ngoài"
"externalTrafficInformURIDesc" = "Cập nhật lưu lượng truy cập được gửi tới URI này."
"fragment" = "Sự phân mảnh"
"fragmentDesc" = "Kích hoạt phân mảnh cho gói TLS hello"
"fragmentSett" = "Cài đặt phân mảnh"
@@ -316,7 +337,15 @@
"muxSett" = "Mux Cài đặt"
"direct" = "Kết nối trực tiếp"
"directDesc" = "Trực tiếp thiết lập kết nối với tên miền hoặc dải IP của một quốc gia cụ thể."
"notifications" = "Thông báo"
"certs" = "Chứng chỉ"
"externalTraffic" = "Lưu lượng bên ngoài"
"dateAndTime" = "Ngày và giờ"
"proxyAndServer" = "Proxy và máy chủ"
"intervals" = "Khoảng thời gian"
"information" = "Thông tin"
"language" = "Ngôn ngữ"
"telegramBotLanguage" = "Ngôn ngữ của Bot Telegram"
[pages.xray]
"title" = "Cài đặt Xray"
@@ -369,6 +398,15 @@
"dnsLogDesc" = "Có bật nhật ký truy vấn DNS không"
"maskAddress" = "Ẩn Địa Chỉ"
"maskAddressDesc" = "Mặt nạ địa chỉ IP, khi được bật, sẽ tự động thay thế địa chỉ IP xuất hiện trong nhật ký."
"statistics" = "Thống kê"
"statsInboundUplink" = "Thống kê tải lên đầu vào"
"statsInboundUplinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải lên của tất cả các proxy đầu vào."
"statsInboundDownlink" = "Thống kê tải xuống đầu vào"
"statsInboundDownlinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải xuống của tất cả các proxy đầu vào."
"statsOutboundUplink" = "Thống kê tải lên đầu ra"
"statsOutboundUplinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải lên của tất cả các proxy đầu ra."
"statsOutboundDownlink" = "Thống kê tải xuống đầu ra"
"statsOutboundDownlinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải xuống của tất cả các proxy đầu ra."
[pages.xray.rules]
"first" = "Đầu tiên"
@@ -398,6 +436,7 @@
"type" = "Loại"
"bridge" = "Cầu"
"portal" = "Cổng thông tin"
"link" = "Liên kết"
"intercon" = "Kết nối"
"settings" = "cài đặt"
"accountInfo" = "Thông tin tài khoản"
@@ -426,6 +465,14 @@
"enableDesc" = "Kích hoạt máy chủ DNS tích hợp"
"tag" = "Thẻ gửi đến DNS"
"tagDesc" = "Thẻ này sẽ có sẵn dưới dạng thẻ Gửi đến trong quy tắc định tuyến."
"clientIp" = "IP khách hàng"
"clientIpDesc" = "Được sử dụng để thông báo cho máy chủ về vị trí IP được chỉ định trong các truy vấn DNS"
"disableCache" = "Tắt bộ nhớ đệm"
"disableCacheDesc" = "Tắt bộ nhớ đệm DNS"
"disableFallback" = "Tắt Fallback"
"disableFallbackDesc" = "Tắt các truy vấn DNS Fallback"
"disableFallbackIfMatch" = "Tắt Fallback Nếu Khớp"
"disableFallbackIfMatchDesc" = "Tắt các truy vấn DNS Fallback khi danh sách tên miền khớp của máy chủ DNS được kích hoạt"
"strategy" = "Chiến lược truy vấn"
"strategyDesc" = "Chiến lược tổng thể để phân giải tên miền"
"add" = "Thêm máy chủ"
@@ -440,7 +487,7 @@
"poolSize" = "Kích thước bể bơi"
[pages.settings.security]
"admin" = "Quản trị viên"
"admin" = "Thông tin đăng nhập quản trị viên"
"secret" = "Mã thông báo bí mật"
"loginSecurity" = "Bảo mật đăng nhập"
"loginSecurityDesc" = "Bật bước bảo mật đăng nhập bổ sung cho người dùng"
@@ -483,9 +530,9 @@
"status" = "✅ Bot hoạt động bình thường!"
"usage" = "❗ Vui lòng cung cấp văn bản để tìm kiếm!"
"getID" = "🆔 ID của bạn: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Để khởi động lại Xray Core:\r\n<code>/restart force</code>\r\n\r\nĐể tìm kiếm email của khách hàng:\r\n<code>/usage [Email]</code>\r\n\r\nĐể tìm kiếm các nhập (với số liệu thống kê của khách hàng):\r\n<code>/inbound [Ghi chú]</code>\r\n\r\nID Trò chuyện Telegram:\r\n<code>/id</code>"
"helpAdminCommands" = "Để khởi động lại Xray Core:\r\n<code>/restart</code>\r\n\r\nĐể tìm kiếm email của khách hàng:\r\n<code>/usage [Email]</code>\r\n\r\nĐể tìm kiếm các nhập (với số liệu thống kê của khách hàng):\r\n<code>/inbound [Ghi chú]</code>\r\n\r\nID Trò chuyện Telegram:\r\n<code>/id</code>"
"helpClientCommands" = "Để tìm kiếm thống kê, sử dụng lệnh sau:\r\n<code>/usage [Email]</code>\r\n\r\nID Trò chuyện Telegram:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart force</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Hoạt động thành công!"
"restartFailed" = "❗ Lỗi trong quá trình hoạt động.\r\n\r\n<code>Lỗi: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core không chạy."

View File

@@ -44,6 +44,7 @@
"monitor" = "监听"
"certificate" = "数字证书"
"fail" = "失败"
"comment" = "评论"
"success" = "成功"
"getVersion" = "获取版本"
"install" = "安装"
@@ -60,8 +61,15 @@
"secAlertPanelURI" = "面板默认 URI 路径不安全。请配置复杂的 URI 路径。"
"secAlertSubURI" = "订阅默认 URI 路径不安全。请配置复杂的 URI 路径。"
"secAlertSubJsonURI" = "订阅 JSON 默认 URI 路径不安全。请配置复杂的 URI 路径。"
"emptyDnsDesc" = "未添加DNS服务器。"
"emptyFakeDnsDesc" = "未添加Fake DNS服务器。"
"emptyBalancersDesc" = "未添加负载均衡器。"
"emptyReverseDesc" = "未添加反向代理。"
[menu]
"theme" = "主题"
"dark" = "暗色"
"ultraDark" = "超暗色"
"dashboard" = "系统状态"
"inbounds" = "入站列表"
"settings" = "面板设置"
@@ -97,20 +105,25 @@
"connectionTcpCountDesc" = "系统中所有 TCP 连接数"
"connectionUdpCountDesc" = "系统中所有 UDP 连接数"
"connectionCount" = "连接数"
"upSpeed" = "总上传速度"
"downSpeed" = "总下载速度"
"totalSent" = "系统启动以来发送的总数据量"
"totalReceive" = "系统启动以来接收的总数据量"
"ipAddresses" = "IP地址"
"toggleIpVisibility" = "切换IP可见性"
"overallSpeed" = "整体速度"
"upload" = "上传"
"download" = "下载"
"totalData" = "总数据"
"sent" = "已发送"
"received" = "已接收"
"xraySwitchVersionDialog" = "切换 Xray 版本"
"xraySwitchVersionDialogDesc" = "是否切换 Xray 版本至"
"dontRefresh" = "安装中,请勿刷新此页面"
"logs" = "日志"
"config" = "配置"
"backup" = "备份和恢复"
"backup" = "备份"
"backupTitle" = "备份和恢复数据库"
"backupDescription" = "恢复数据库之前建议进行备份"
"exportDatabase" = "备份"
"exportDatabaseDesc" = "点击下载包含当前数据库备份的 .db 文件到您的设备。"
"importDatabase" = "恢复"
"importDatabaseDesc" = "点击选择并上传设备中的 .db 文件以从备份恢复数据库。"
[pages.inbounds]
"title" = "入站列表"
@@ -129,6 +142,8 @@
"resetTraffic" = "重置流量"
"addInbound" = "添加入站"
"generalActions" = "通用操作"
"autoRefresh" = "自动刷新"
"autoRefreshInterval" = "间隔"
"create" = "添加"
"update" = "修改"
"modifyInbound" = "修改入站"
@@ -286,6 +301,8 @@
"subSettings" = "订阅设置"
"subEnable" = "启用订阅服务"
"subEnableDesc" = "启用订阅服务功能"
"subTitle" = "订阅标题"
"subTitleDesc" = "在VPN客户端中显示的标题"
"subListen" = "监听 IP"
"subListenDesc" = "订阅服务监听的 IP 地址(留空表示监听所有 IP"
"subPort" = "监听端口"
@@ -306,6 +323,10 @@
"subShowInfoDesc" = "客户端应用中将显示剩余流量和日期信息"
"subURI" = "反向代理 URI"
"subURIDesc" = "用于代理后面的订阅 URL 的 URI 路径"
"externalTrafficInformEnable" = "外部交通通知"
"externalTrafficInformEnableDesc" = "每次流量更新时通知外部 API"
"externalTrafficInformURI" = "外部流量通知 URI"
"externalTrafficInformURIDesc" = "流量更新将发送到此 URI"
"fragment" = "分片"
"fragmentDesc" = "启用 TLS hello 数据包分片"
"fragmentSett" = "设置"
@@ -316,7 +337,15 @@
"muxSett" = "复用器设置"
"direct" = "直接连接"
"directDesc" = "直接与特定国家的域或IP范围建立连接"
"notifications" = "通知"
"certs" = "证书"
"externalTraffic" = "外部流量"
"dateAndTime" = "日期和时间"
"proxyAndServer" = "代理和服务器"
"intervals" = "间隔"
"information" = "信息"
"language" = "语言"
"telegramBotLanguage" = "Telegram 机器人语言"
[pages.xray]
"title" = "Xray 配置"
@@ -369,6 +398,15 @@
"dnsLogDesc" = "是否启用 DNS 查询日志"
"maskAddress" = "隐藏地址"
"maskAddressDesc" = "IP 地址掩码,启用时会自动替换日志中出现的 IP 地址。"
"statistics" = "统计"
"statsInboundUplink" = "入站上传统计"
"statsInboundUplinkDesc" = "启用所有入站代理的上行流量统计收集。"
"statsInboundDownlink" = "入站下载统计"
"statsInboundDownlinkDesc" = "启用所有入站代理的下行流量统计收集。"
"statsOutboundUplink" = "出站上传统计"
"statsOutboundUplinkDesc" = "启用所有出站代理的上行流量统计收集。"
"statsOutboundDownlink" = "出站下载统计"
"statsOutboundDownlinkDesc" = "启用所有出站代理的下行流量统计收集。"
[pages.xray.rules]
"first" = "置顶"
@@ -398,6 +436,7 @@
"type" = "类型"
"bridge" = "Bridge"
"portal" = "Portal"
"link" = "链接"
"intercon" = "互连"
"settings" = "设置"
"accountInfo" = "帐户信息"
@@ -426,6 +465,14 @@
"enableDesc" = "启用内置 DNS 服务器"
"tag" = "DNS 入站标签"
"tagDesc" = "此标签将在路由规则中可用作入站标签"
"clientIp" = "客户端IP"
"clientIpDesc" = "用于在DNS查询期间通知服务器指定的IP位置"
"disableCache" = "禁用缓存"
"disableCacheDesc" = "禁用DNS缓存"
"disableFallback" = "禁用回退"
"disableFallbackDesc" = "禁用回退DNS查询"
"disableFallbackIfMatch" = "匹配时禁用回退"
"disableFallbackIfMatchDesc" = "当DNS服务器的匹配域名列表命中时禁用回退DNS查询"
"strategy" = "查询策略"
"strategyDesc" = "解析域名的总体策略"
"add" = "添加服务器"
@@ -440,7 +487,7 @@
"poolSize" = "池大小"
[pages.settings.security]
"admin" = "管理员"
"admin" = "管理员凭据"
"secret" = "安全令牌"
"loginSecurity" = "登录安全"
"loginSecurityDesc" = "添加额外的身份验证以提高安全性"
@@ -483,9 +530,9 @@
"status" = "✅ 机器人正常运行!"
"usage" = "❗ 请输入要搜索的文本!"
"getID" = "🆔 您的 ID 为:<code>{{ .ID }}</code>"
"helpAdminCommands" = "要重新启动 Xray Core\r\n<code>/restart force</code>\r\n\r\n要搜索客户电子邮件\r\n<code>/usage [电子邮件]</code>\r\n\r\n要搜索入站带有客户统计数据\r\n<code>/inbound [备注]</code>\r\n\r\nTelegram聊天ID\r\n<code>/id</code>"
"helpAdminCommands" = "要重新启动 Xray Core\r\n<code>/restart</code>\r\n\r\n要搜索客户电子邮件\r\n<code>/usage [电子邮件]</code>\r\n\r\n要搜索入站带有客户统计数据\r\n<code>/inbound [备注]</code>\r\n\r\nTelegram聊天ID\r\n<code>/id</code>"
"helpClientCommands" = "要搜索统计数据,请使用以下命令:\r\n<code>/usage [电子邮件]</code>\r\n\r\nTelegram聊天ID\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart force</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ 操作成功!"
"restartFailed" = "❗ 操作错误。\r\n\r\n<code>错误: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core 未运行。"

View File

@@ -44,6 +44,7 @@
"monitor" = "監聽"
"certificate" = "憑證"
"fail" = "失敗"
"comment" = "評論"
"success" = "成功"
"getVersion" = "獲取版本"
"install" = "安裝"
@@ -60,8 +61,15 @@
"secAlertPanelURI" = "面板預設 URI 路徑不安全。請配置複雜的 URI 路徑。"
"secAlertSubURI" = "訂閱預設 URI 路徑不安全。請配置複雜的 URI 路徑。"
"secAlertSubJsonURI" = "訂閱 JSON 預設 URI 路徑不安全。請配置複雜的 URI 路徑。"
"emptyDnsDesc" = "未添加DNS伺服器。"
"emptyFakeDnsDesc" = "未添加Fake DNS伺服器。"
"emptyBalancersDesc" = "未添加負載平衡器。"
"emptyReverseDesc" = "未添加反向代理。"
[menu]
"theme" = "主題"
"dark" = "深色"
"ultraDark" = "超深色"
"dashboard" = "系統狀態"
"inbounds" = "入站列表"
"settings" = "面板設定"
@@ -97,10 +105,14 @@
"connectionTcpCountDesc" = "系統中所有 TCP 連線數"
"connectionUdpCountDesc" = "系統中所有 UDP 連線數"
"connectionCount" = "連線數"
"upSpeed" = "總上傳速度"
"downSpeed" = "總下載速度"
"totalSent" = "系統啟動以來傳送的總資料量"
"totalReceive" = "系統啟動以來接收的總資料量"
"ipAddresses" = "IP地址"
"toggleIpVisibility" = "切換IP可見性"
"overallSpeed" = "整體速度"
"upload" = "上傳"
"download" = "下載"
"totalData" = "總數據"
"sent" = "已發送"
"received" = "已接收"
"xraySwitchVersionDialog" = "切換 Xray 版本"
"xraySwitchVersionDialogDesc" = "是否切換 Xray 版本至"
"dontRefresh" = "安裝中,請勿重新整理此頁面"
@@ -108,9 +120,10 @@
"config" = "配置"
"backup" = "備份和恢復"
"backupTitle" = "備份和恢復資料庫"
"backupDescription" = "恢復資料庫之前建議進行備份"
"exportDatabase" = "備份"
"exportDatabaseDesc" = "點擊下載包含當前資料庫備份的 .db 文件到您的設備。"
"importDatabase" = "恢復"
"importDatabaseDesc" = "點擊選擇並上傳設備中的 .db 文件以從備份恢復資料庫。"
[pages.inbounds]
"title" = "入站列表"
@@ -129,6 +142,8 @@
"resetTraffic" = "重置流量"
"addInbound" = "新增入站"
"generalActions" = "通用操作"
"autoRefresh" = "自動刷新"
"autoRefreshInterval" = "間隔"
"create" = "新增"
"update" = "修改"
"modifyInbound" = "修改入站"
@@ -286,6 +301,8 @@
"subSettings" = "訂閱設定"
"subEnable" = "啟用訂閱服務"
"subEnableDesc" = "啟用訂閱服務功能"
"subTitle" = "訂閱標題"
"subTitleDesc" = "在VPN客戶端中顯示的標題"
"subListen" = "監聽 IP"
"subListenDesc" = "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP"
"subPort" = "監聽埠"
@@ -306,6 +323,10 @@
"subShowInfoDesc" = "客戶端應用中將顯示剩餘流量和日期資訊"
"subURI" = "反向代理 URI"
"subURIDesc" = "用於代理後面的訂閱 URL 的 URI 路徑"
"externalTrafficInformEnable" = "外部交通通知"
"externalTrafficInformEnableDesc" = "每次流量更新時通知外部 API"
"externalTrafficInformURI" = "外部流量通知 URI"
"externalTrafficInformURIDesc" = "流量更新將會傳送到此 URI"
"fragment" = "分片"
"fragmentDesc" = "啟用 TLS hello 資料包分片"
"fragmentSett" = "設定"
@@ -316,7 +337,15 @@
"muxSett" = "複用器設定"
"direct" = "直接連線"
"directDesc" = "直接與特定國家的域或IP範圍建立連線"
"notifications" = "通知"
"certs" = "證書"
"externalTraffic" = "外部流量"
"dateAndTime" = "日期和時間"
"proxyAndServer" = "代理和伺服器"
"intervals" = "間隔"
"information" = "資訊"
"language" = "語言"
"telegramBotLanguage" = "Telegram 機器人語言"
[pages.xray]
"title" = "Xray 配置"
@@ -369,6 +398,15 @@
"dnsLogDesc" = "是否啟用 DNS 查詢日誌"
"maskAddress" = "隱藏地址"
"maskAddressDesc" = "IP 地址掩碼,啟用時會自動替換日誌中出現的 IP 地址。"
"statistics" = "統計"
"statsInboundUplink" = "入站上傳統計"
"statsInboundUplinkDesc" = "啟用所有入站代理的上行流量統計收集。"
"statsInboundDownlink" = "入站下載統計"
"statsInboundDownlinkDesc" = "啟用所有入站代理的下行流量統計收集。"
"statsOutboundUplink" = "出站上傳統計"
"statsOutboundUplinkDesc" = "啟用所有出站代理的上行流量統計收集。"
"statsOutboundDownlink" = "出站下載統計"
"statsOutboundDownlinkDesc" = "啟用所有出站代理的下行流量統計收集。"
[pages.xray.rules]
"first" = "置頂"
@@ -398,6 +436,7 @@
"type" = "類型"
"bridge" = "Bridge"
"portal" = "Portal"
"link" = "連結"
"intercon" = "互連"
"settings" = "設定"
"accountInfo" = "帳戶資訊"
@@ -426,6 +465,14 @@
"enableDesc" = "啟用內建 DNS 伺服器"
"tag" = "DNS 入站標籤"
"tagDesc" = "此標籤將在路由規則中可用作入站標籤"
"clientIp" = "客戶端IP"
"clientIpDesc" = "用於在DNS查詢期間通知伺服器指定的IP位置"
"disableCache" = "禁用快取"
"disableCacheDesc" = "禁用DNS快取"
"disableFallback" = "禁用回退"
"disableFallbackDesc" = "禁用回退DNS查詢"
"disableFallbackIfMatch" = "匹配時禁用回退"
"disableFallbackIfMatchDesc" = "當DNS伺服器的匹配域名列表命中時禁用回退DNS查詢"
"strategy" = "查詢策略"
"strategyDesc" = "解析域名的總體策略"
"add" = "新增伺服器"
@@ -440,7 +487,7 @@
"poolSize" = "池大小"
[pages.settings.security]
"admin" = "管理員"
"admin" = "管理員憑證"
"secret" = "安全令牌"
"loginSecurity" = "登入安全"
"loginSecurityDesc" = "新增額外的身份驗證以提高安全性"
@@ -483,9 +530,9 @@
"status" = "✅ 機器人正常執行!"
"usage" = "❗ 請輸入要搜尋的文字!"
"getID" = "🆔 您的 ID 為:<code>{{ .ID }}</code>"
"helpAdminCommands" = "要重新啟動 Xray Core\r\n<code>/restart force</code>\r\n\r\n要搜尋客戶電子郵件\r\n<code>/usage [電子郵件]</code>\r\n\r\n要搜尋入站帶有客戶統計資料\r\n<code>/inbound [備註]</code>\r\n\r\nTelegram聊天ID\r\n<code>/id</code>"
"helpAdminCommands" = "要重新啟動 Xray Core\r\n<code>/restart</code>\r\n\r\n要搜尋客戶電子郵件\r\n<code>/usage [電子郵件]</code>\r\n\r\n要搜尋入站帶有客戶統計資料\r\n<code>/inbound [備註]</code>\r\n\r\nTelegram聊天ID\r\n<code>/id</code>"
"helpClientCommands" = "要搜尋統計資料,請使用以下命令:\r\n<code>/usage [電子郵件]</code>\r\n\r\nTelegram聊天ID\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart force</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ 操作成功!"
"restartFailed" = "❗ 操作錯誤。\r\n\r\n<code>錯誤: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core 未運行。"

260
x-ui.sh
View File

@@ -60,8 +60,8 @@ elif [[ "${release}" == "centos" ]]; then
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 2004 ]]; then
echo -e "${red} Please use Ubuntu 20 or higher version!${plain}\n" && exit 1
if [[ ${os_version} -lt 2204 ]]; then
echo -e "${red} Please use Ubuntu 22 or higher version!${plain}\n" && exit 1
fi
elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then
@@ -87,10 +87,14 @@ elif [[ "${release}" == "ol" ]]; then
if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use Oracle Linux 8 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "virtuozzo" ]]; then
if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use Virtuozzo Linux 8 or higher ${plain}\n" && exit 1
fi
else
echo -e "${red}Your operating system is not supported by this script.${plain}\n"
echo "Please ensure you are using one of the following supported operating systems:"
echo "- Ubuntu 20.04+"
echo "- Ubuntu 22.04+"
echo "- Debian 11+"
echo "- CentOS 8+"
echo "- OpenEuler 22.03+"
@@ -104,6 +108,7 @@ else
echo "- Oracle Linux 8+"
echo "- OpenSUSE Tumbleweed"
echo "- Amazon Linux 2023"
echo "- Virtuozzo Linux 8+"
exit 1
fi
@@ -180,7 +185,7 @@ update_menu() {
return 0
fi
wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
wget -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
chmod +x /usr/local/x-ui/x-ui.sh
chmod +x /usr/bin/x-ui
@@ -280,7 +285,7 @@ reset_webbasepath() {
# Apply the new web base path setting
/usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}" >/dev/null 2>&1
echo -e "Web base path has been reset to: ${green}${config_webBasePath}${plain}"
echo -e "${green}Please use the new web base path to access the panel.${plain}"
restart
@@ -440,7 +445,7 @@ show_log() {
1)
journalctl -u x-ui -e --no-pager -f -p debug
if [[ $# == 0 ]]; then
before_show_menu
before_show_menu
fi
;;
2)
@@ -458,9 +463,9 @@ show_log() {
show_banlog() {
local system_log="/var/log/fail2ban.log"
echo -e "${green}Checking ban logs...${plain}\n"
if ! systemctl is-active --quiet fail2ban; then
echo -e "${red}Fail2ban service is not running!${plain}\n"
return 1
@@ -504,7 +509,7 @@ bbr_menu() {
disable_bbr
bbr_menu
;;
*)
*)
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
bbr_menu
;;
@@ -547,7 +552,7 @@ enable_bbr() {
centos | almalinux | rocky | ol)
yum -y update && yum -y install ca-certificates
;;
fedora | amzn)
fedora | amzn | virtuozzo)
dnf -y update && dnf -y install ca-certificates
;;
arch | manjaro | parch)
@@ -575,14 +580,14 @@ enable_bbr() {
}
update_shell() {
wget -O /usr/bin/x-ui -N --no-check-certificate https://github.com/MHSanaei/3x-ui/raw/main/x-ui.sh
wget -O /usr/bin/x-ui -N https://github.com/MHSanaei/3x-ui/raw/main/x-ui.sh
if [[ $? != 0 ]]; then
echo ""
LOGE "Failed to download script, Please check whether the machine can connect Github"
before_show_menu
else
chmod +x /usr/bin/x-ui
LOGI "Upgrade script succeeded, Please rerun the script"
LOGI "Upgrade script succeeded, Please rerun the script"
before_show_menu
fi
}
@@ -683,12 +688,13 @@ show_xray_status() {
}
firewall_menu() {
echo -e "${green}\t1.${plain} Install Firewall"
echo -e "${green}\t2.${plain} Port List"
echo -e "${green}\t3.${plain} Open Ports"
echo -e "${green}\t4.${plain} Delete Ports from List"
echo -e "${green}\t5.${plain} Disable Firewall"
echo -e "${green}\t6.${plain} Firewall Status"
echo -e "${green}\t1.${plain} ${green}Install${plain} Firewall"
echo -e "${green}\t2.${plain} Port List [numbered]"
echo -e "${green}\t3.${plain} ${green}Open${plain} Ports"
echo -e "${green}\t4.${plain} ${red}Delete${plain} Ports from List"
echo -e "${green}\t5.${plain} ${green}Enable${plain} Firewall"
echo -e "${green}\t6.${plain} ${red}Disable${plain} Firewall"
echo -e "${green}\t7.${plain} Firewall Status"
echo -e "${green}\t0.${plain} Back to Main Menu"
read -p "Choose an option: " choice
case "$choice" in
@@ -712,14 +718,18 @@ firewall_menu() {
firewall_menu
;;
5)
ufw disable
ufw enable
firewall_menu
;;
6)
ufw disable
firewall_menu
;;
7)
ufw status verbose
firewall_menu
;;
*)
*)
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
firewall_menu
;;
@@ -794,50 +804,84 @@ open_ports() {
}
delete_ports() {
# Prompt the user to enter the ports they want to delete
read -p "Enter the ports you want to delete (e.g. 80,443,2053 or range 400-500): " ports
# Display current rules with numbers
echo "Current UFW rules:"
ufw status numbered
# Check if the input is valid
if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then
echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2
# Ask the user how they want to delete rules
echo "Do you want to delete rules by:"
echo "1) Rule numbers"
echo "2) Ports"
read -p "Enter your choice (1 or 2): " choice
if [[ $choice -eq 1 ]]; then
# Deleting by rule numbers
read -p "Enter the rule numbers you want to delete (1, 2, etc.): " rule_numbers
# Validate the input
if ! [[ $rule_numbers =~ ^([0-9]+)(,[0-9]+)*$ ]]; then
echo "Error: Invalid input. Please enter a comma-separated list of rule numbers." >&2
exit 1
fi
# Split numbers into an array
IFS=',' read -ra RULE_NUMBERS <<<"$rule_numbers"
for rule_number in "${RULE_NUMBERS[@]}"; do
# Delete the rule by number
ufw delete "$rule_number" || echo "Failed to delete rule number $rule_number"
done
echo "Selected rules have been deleted."
elif [[ $choice -eq 2 ]]; then
# Deleting by ports
read -p "Enter the ports you want to delete (e.g. 80,443,2053 or range 400-500): " ports
# Validate the input
if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then
echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2
exit 1
fi
# Split ports into an array
IFS=',' read -ra PORT_LIST <<<"$ports"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
# Split the port range
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# Delete the port range
ufw delete allow $start_port:$end_port/tcp
ufw delete allow $start_port:$end_port/udp
else
# Delete a single port
ufw delete allow "$port"
fi
done
# Confirmation of deletion
echo "Deleted the specified ports:"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# Check if the port range has been deleted
(ufw status | grep -q "$start_port:$end_port") || echo "$start_port-$end_port"
else
# Check if the individual port has been deleted
(ufw status | grep -q "$port") || echo "$port"
fi
done
else
echo "${red}Error:${plain} Invalid choice. Please enter 1 or 2." >&2
exit 1
fi
# Delete the specified ports using ufw
IFS=',' read -ra PORT_LIST <<<"$ports"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
# Split the range into start and end ports
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# Delete the port range
ufw delete allow $start_port:$end_port/tcp
ufw delete allow $start_port:$end_port/udp
else
ufw delete allow "$port"
fi
done
# Confirm that the ports are deleted
echo "Deleted the specified ports:"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# Check if the port range has been successfully deleted
(ufw status | grep -q "$start_port:$end_port") || echo "$start_port-$end_port"
else
# Check if the individual port has been successfully deleted
(ufw status | grep -q "$port") || echo "$port"
fi
done
}
update_geo() {
echo -e "${green}\t1.${plain} Loyalsoldier (geoip.dat, geosite.dat)"
echo -e "${green}\t2.${plain} chocolate4u (geoip_IR.dat, geosite_IR.dat)"
echo -e "${green}\t3.${plain} vuong2023 (geoip_VN.dat, geosite_VN.dat)"
echo -e "${green}\t3.${plain} runetfreedom (geoip_RU.dat, geosite_RU.dat)"
echo -e "${green}\t0.${plain} Back to Main Menu"
read -p "Choose an option: " choice
@@ -865,10 +909,10 @@ update_geo() {
;;
3)
systemctl stop x-ui
rm -f geoip_VN.dat geosite_VN.dat
wget -O geoip_VN.dat -N https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_VN.dat -N https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geosite.dat
echo -e "${green}vuong2023 datasets have been updated successfully!${plain}"
rm -f geoip_RU.dat geosite_RU.dat
wget -O geoip_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -O geosite_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
echo -e "${green}runetfreedom datasets have been updated successfully!${plain}"
restart
;;
*)
@@ -1029,7 +1073,7 @@ ssl_cert_issue() {
centos | almalinux | rocky | ol)
yum -y update && yum -y install socat
;;
fedora | amzn)
fedora | amzn | virtuozzo)
dnf -y update && dnf -y install socat
;;
arch | manjaro | parch)
@@ -1083,7 +1127,7 @@ ssl_cert_issue() {
# issue the certificate
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort}
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
if [ $? -ne 0 ]; then
LOGE "Issuing certificate failed, please check logs."
rm -rf ~/.acme.sh/${domain}
@@ -1092,10 +1136,36 @@ ssl_cert_issue() {
LOGE "Issuing certificate succeeded, installing certificates..."
fi
reloadCmd="x-ui restart"
LOGI "Default --reloadcmd for ACME is: ${yellow}x-ui restart"
LOGI "This command will run on every certificate issue and renew."
read -p "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd
if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then
echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; x-ui restart"
echo -e "${green}\t2.${plain} Input your own command"
echo -e "${green}\t0.${plain} Keep default reloadcmd"
read -p "Choose an option: " choice
case "$choice" in
1)
LOGI "Reloadcmd is: systemctl reload nginx ; x-ui restart"
reloadCmd="systemctl reload nginx ; x-ui restart"
;;
2)
LOGD "It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails"
read -p "Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): " reloadCmd
LOGI "Your reloadcmd is: ${reloadCmd}"
;;
*)
LOGI "Keep default reloadcmd"
;;
esac
fi
# install the certificate
~/.acme.sh/acme.sh --installcert -d ${domain} \
--key-file /root/cert/${domain}/privkey.pem \
--fullchain-file /root/cert/${domain}/fullchain.pem
--fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}"
if [ $? -ne 0 ]; then
LOGE "Installing certificate failed, exiting."
@@ -1164,13 +1234,6 @@ ssl_cert_issue_CF() {
fi
CF_Domain=""
certPath="/root/cert-CF"
if [ ! -d "$certPath" ]; then
mkdir -p $certPath
else
rm -rf $certPath
mkdir -p $certPath
fi
LOGD "Please set a domain name:"
read -p "Input your domain here: " CF_Domain
@@ -1198,7 +1261,7 @@ ssl_cert_issue_CF() {
export CF_Email="${CF_AccountEmail}"
# Issue the certificate using Cloudflare DNS
~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log
~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log --force
if [ $? -ne 0 ]; then
LOGE "Certificate issuance failed, script exiting..."
exit 1
@@ -1206,17 +1269,47 @@ ssl_cert_issue_CF() {
LOGI "Certificate issued successfully, Installing..."
fi
# Install the certificate
mkdir -p ${certPath}/${CF_Domain}
# Install the certificate
certPath="/root/cert/${CF_Domain}"
if [ -d "$certPath" ]; then
rm -rf ${certPath}
fi
mkdir -p ${certPath}
if [ $? -ne 0 ]; then
LOGE "Failed to create directory: ${certPath}/${CF_Domain}"
LOGE "Failed to create directory: ${certPath}"
exit 1
fi
~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \
--fullchain-file ${certPath}/${CF_Domain}/fullchain.pem \
--key-file ${certPath}/${CF_Domain}/privkey.pem
reloadCmd="x-ui restart"
LOGI "Default --reloadcmd for ACME is: ${yellow}x-ui restart"
LOGI "This command will run on every certificate issue and renew."
read -p "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd
if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then
echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; x-ui restart"
echo -e "${green}\t2.${plain} Input your own command"
echo -e "${green}\t0.${plain} Keep default reloadcmd"
read -p "Choose an option: " choice
case "$choice" in
1)
LOGI "Reloadcmd is: systemctl reload nginx ; x-ui restart"
reloadCmd="systemctl reload nginx ; x-ui restart"
;;
2)
LOGD "It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails"
read -p "Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): " reloadCmd
LOGI "Your reloadcmd is: ${reloadCmd}"
;;
*)
LOGI "Keep default reloadcmd"
;;
esac
fi
~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \
--key-file ${certPath}/privkey.pem \
--fullchain-file ${certPath}/fullchain.pem --reloadcmd "${reloadCmd}"
if [ $? -ne 0 ]; then
LOGE "Certificate installation failed, script exiting..."
exit 1
@@ -1231,15 +1324,15 @@ ssl_cert_issue_CF() {
exit 1
else
LOGI "The certificate is installed and auto-renewal is turned on. Specific information is as follows:"
ls -lah ${certPath}/${CF_Domain}
chmod 755 ${certPath}/${CF_Domain}
ls -lah ${certPath}/*
chmod 755 ${certPath}/*
fi
# Prompt user to set panel paths after successful certificate installation
read -p "Would you like to set this certificate for the panel? (y/n): " setPanel
if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then
local webCertFile="${certPath}/${CF_Domain}/fullchain.pem"
local webKeyFile="${certPath}/${CF_Domain}/privkey.pem"
local webCertFile="${certPath}/fullchain.pem"
local webKeyFile="${certPath}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
/usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
@@ -1497,7 +1590,7 @@ install_iplimit() {
yum update -y && yum install epel-release -y
yum -y install fail2ban
;;
fedora | amzn)
fedora | amzn | virtuozzo)
dnf -y update && dnf -y install fail2ban
;;
arch | manjaro | parch)
@@ -1541,7 +1634,6 @@ install_iplimit() {
# Launching fail2ban
if ! systemctl is-active --quiet fail2ban; then
systemctl start fail2ban
systemctl enable fail2ban
else
systemctl restart fail2ban
fi
@@ -1578,7 +1670,7 @@ remove_iplimit() {
yum remove fail2ban -y
yum autoremove -y
;;
fedora | amzn)
fedora | amzn | virtuozzo)
dnf remove fail2ban -y
dnf autoremove -y
;;

View File

@@ -92,7 +92,7 @@ func (x *XrayAPI) DelInbound(tag string) error {
return err
}
func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]interface{}) error {
func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]any) error {
var account *serial.TypedMessage
switch Protocol {
case "vmess":

View File

@@ -32,32 +32,56 @@ func (lw *LogWriter) Write(m []byte) (n int, err error) {
return len(m), nil
}
regex := regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[([^\]]+)\] (.+)$`)
messages := strings.Split(message, "\n")
regex := regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}\.\d{6}) \[([^\]]+)\] (.+)$`)
messages := strings.SplitSeq(message, "\n")
for _, msg := range messages {
for msg := range messages {
matches := regex.FindStringSubmatch(msg)
if len(matches) > 3 {
level := matches[2]
msgBody := matches[3]
msgBodyLower := strings.ToLower(msgBody)
// Map the level to the appropriate logger function
switch level {
case "Debug":
if strings.Contains(msgBodyLower, "tls handshake error") ||
strings.Contains(msgBodyLower, "connection ends") {
logger.Debug("XRAY: " + msgBody)
case "Info":
logger.Info("XRAY: " + msgBody)
case "Warning":
logger.Warning("XRAY: " + msgBody)
case "Error":
lw.lastLine = ""
continue
}
if strings.Contains(msgBodyLower, "failed") {
logger.Error("XRAY: " + msgBody)
default:
logger.Debug("XRAY: " + msg)
} else {
switch level {
case "Debug":
logger.Debug("XRAY: " + msgBody)
case "Info":
logger.Info("XRAY: " + msgBody)
case "Warning":
logger.Warning("XRAY: " + msgBody)
case "Error":
logger.Error("XRAY: " + msgBody)
default:
logger.Debug("XRAY: " + msg)
}
}
lw.lastLine = ""
} else if msg != "" {
logger.Debug("XRAY: " + msg)
msgLower := strings.ToLower(msg)
if strings.Contains(msgLower, "tls handshake error") ||
strings.Contains(msgLower, "connection ends") {
logger.Debug("XRAY: " + msg)
lw.lastLine = msg
continue
}
if strings.Contains(msgLower, "failed") {
logger.Error("XRAY: " + msg)
} else {
logger.Debug("XRAY: " + msg)
}
lw.lastLine = msg
}
}

View File

@@ -64,7 +64,7 @@ func GetAccessLogPath() (string, error) {
return "", err
}
jsonConfig := map[string]interface{}{}
jsonConfig := map[string]any{}
err = json.Unmarshal([]byte(config), &jsonConfig)
if err != nil {
logger.Warningf("Failed to parse JSON configuration: %s", err)
@@ -72,7 +72,7 @@ func GetAccessLogPath() (string, error) {
}
if jsonConfig["log"] != nil {
jsonLog := jsonConfig["log"].(map[string]interface{})
jsonLog := jsonConfig["log"].(map[string]any)
if jsonLog["access"] != nil {
accessLogPath := jsonLog["access"].(string)
return accessLogPath, nil