Compare commits

...

116 Commits

Author SHA1 Message Date
mhsanaei
5408a2f82c v2.8.0 2025-09-14 22:09:36 +02:00
mhsanaei
c8d71ea748 minify css 2025-09-14 20:05:15 +02:00
mhsanaei
46de886b53 windows: error filter 2025-09-14 20:04:12 +02:00
Sanaei
6d41320ed7 Merge pull request #3466 from MHSanaei/Subscription
Subscription,tgbot,rule
2025-09-14 20:03:32 +02:00
mhsanaei
bf9d2e6aeb rule: Vless Route 2025-09-14 19:53:05 +02:00
mhsanaei
ed96fa090b tgbot: subscription,qrcode, link 2025-09-14 19:51:57 +02:00
Alireza Ahmadi
3ac1d7f546 enhancements 2025-09-14 19:44:26 +02:00
mhsanaei
10025ffa66 Subscription 2025-09-14 18:56:31 +02:00
mhsanaei
5ee62b25ca clean html files
move styles to css
2025-09-12 18:46:20 +02:00
mhsanaei
311d11a3c1 cookie: MaxAge
and minor changes
2025-09-12 13:04:36 +02:00
Copilot
40b6d7707a Fix critical bugs in ObjectUtil.equals() and filterInbounds() functions (#3451)
* Initial plan

* Fix ObjectUtil.equals asymmetric comparison and filterInbounds null pointer bugs

Co-authored-by: MHSanaei <33454419+MHSanaei@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MHSanaei <33454419+MHSanaei@users.noreply.github.com>
2025-09-11 11:48:30 +02:00
mhsanaei
cbf316db31 Update check_client_ip_job.go 2025-09-11 11:10:17 +02:00
Ivan Korney
33a36ada4b fix: ru top level domain regexp option (#3450) 2025-09-11 07:06:51 +02:00
mhsanaei
82ddd10627 Fixed: update Xray Core on Windows 2025-09-10 21:12:37 +02:00
mhsanaei
2401c99817 rules: source to sourceIP 2025-09-10 18:30:40 +02:00
mhsanaei
2f36a4047c API: delClientByEmail 2025-09-10 16:36:12 +02:00
mhsanaei
dc3b0d218a Xray Core v25.9.11 2025-09-10 14:39:07 +02:00
mhsanaei
610d29765a outbound: mixed to socks 2025-09-10 12:19:09 +02:00
mhsanaei
b1ea8005e4 v2.7.0 2025-09-10 08:43:46 +02:00
mhsanaei
3f0bfa2472 Remove the buggy version of Xray core 2025-09-10 08:43:10 +02:00
mhsanaei
1e2ff650ad Xray Core v25.9.10 + GO v1.25.1 2025-09-10 08:40:08 +02:00
Sanaei
c2d6dd923f windows workflow (#3439) 2025-09-09 18:41:44 +02:00
mhsanaei
723ec25fb2 renamed dest to target 2025-09-09 14:35:21 +02:00
mhsanaei
7dc52e9a53 dokodemo-door, socks renamed to mixed, tunnel 2025-09-09 13:57:40 +02:00
Sanaei
fe9f0d1d0e api (#3434) 2025-09-09 02:32:05 +02:00
mhsanaei
18d74d54ca outbound: ECH Config List 2025-09-08 21:25:30 +02:00
mhsanaei
c7ba6ae909 add clear button 2025-09-08 21:17:48 +02:00
mhsanaei
3edf79e589 actions/setup-go@v6 2025-09-08 14:33:04 +02:00
mhsanaei
5420e643cf minor change 2025-09-08 14:32:49 +02:00
mhsanaei
9fcd0387ca Update release.yml 2025-09-08 01:12:27 +02:00
mhsanaei
7b039d219e v2.6.8 2025-09-08 00:29:30 +02:00
mhsanaei
dbec28b915 remove unsupported cipher method 2025-09-07 22:55:37 +02:00
mhsanaei
e5126806d7 xray core v25.9.5 2025-09-07 22:45:20 +02:00
Sanaei
b008ff4ad2 Vlessenc (#3426)
* mlkem768

* VlessEnc
2025-09-07 22:35:38 +02:00
Danil S.
da6b89fdcd chore: login improvements (#3408)
- added client-side form validation
- now with slow internet otp input does not appear later than all input
2025-09-04 12:11:52 +02:00
MHSanaei
d7882c25d1 removed domainMatcher 2025-09-04 12:07:39 +02:00
Ali Golzar
ed2a0a0bcf fix: prevent client updated_at from resetting when parent inbound is updated 2025-09-02 13:30:41 +03:30
Ali Golzar
4a0914cb1e feat: add "Last Online" column to client list and modal (Closes #3402) (#3405)
* feat: persist client last online and expose API

* feat(ui): show client last online in table and info modal

* i18n: add “Last Online” across locales

* chore: format timestamps as HH:mm:ss
2025-08-31 18:33:50 +02:00
Darkcyankitty
664269d513 x-ui.service hardneing (#3397) 2025-08-31 15:26:46 +02:00
Ali Golzar
d0796b26c9 fix(ui): hide Created/Updated columns and fix issues in small displays (#3400)
- Hide the “Created” and “Updated” columns in the clients
- Ensures the “All-time Traffic” column no longer overlaps with adjacent columns.
- Improves layout readability and prevents UI cluttering after the v2.6.7 update.

Closes #3399
2025-08-30 23:01:57 +02:00
mhsanaei
2750f46c01 v2.6.7 2025-08-30 16:05:33 +02:00
mhsanaei
023eb513e4 Xray Core v25.8.29 2025-08-30 10:03:32 +02:00
mhsanaei
0c7b59ed47 removed: Allocate 2025-08-28 10:15:04 +02:00
Ali Golzar
3087c1b123 Add all-time traffic for inbounds and clients (#3387)
* feat(db): add allTime field to Inbound and ClientTraffic models

* feat(inbound): increment all_time for inbounds and clients on traffic updates

calculate correct all_time traffic on migrate command

* feat(ui): show all-time traffic column for inbounds and its clients

* i18n: add pages.inbounds.allTimeTraffic label across locales

* Add All Time Traffic Usage in inbounds page top banner
2025-08-28 01:10:50 +02:00
Ali Golzar
2198397197 Created / Updated fields for clients (#3384)
* feat(backend): add created_at/updated_at to clients and maintain on create/update
backfill existing clients and set updated_at on mutations

* feat(frontend): carry created_at/updated_at in client models and round-trip via JSON

* feat(frontend): display Created and Updated columns in client table with proper date formatting

* i18n: add pages.inbounds.createdAt/updatedAt across all locales

* Update inbound.go

Remove duplicate code
2025-08-27 19:30:49 +02:00
Igor Finagin
d10c312e62 AutoFill OTP (#3381)
https://developer.apple.com/documentation/security/enabling-password-autofill-on-an-html-input-element
2025-08-25 13:42:02 +02:00
mhsanaei
24a3411465 more list for public IP address 2025-08-21 14:24:25 +02:00
Alireza Ahmand
2198e7a28f feat: Add remaining time to tgbot #3355 (#3360) 2025-08-17 13:43:25 +02:00
mhsanaei
6b23b416a7 minor changes 2025-08-17 13:37:49 +02:00
mhsanaei
16f53ce4c2 go v1.25 2025-08-17 12:27:21 +02:00
mhsanaei
27445b30e9 DNS outbound: Set "reject" as the default value for nonIPQuery 2025-08-17 12:22:33 +02:00
mhsanaei
3d0212c21d fix: fail2ban on Debian 12 #1701 2025-08-15 13:34:02 +02:00
mhsanaei
978755960f actions/checkout from 4 to 5 2025-08-14 18:41:53 +02:00
mhsanaei
9b51e9a5c5 Freedom: Add maxSplit fragment option; Add applyTo noises option 2025-08-14 18:38:56 +02:00
fgsfds
6879a8fbcb Moved DB to same app folder on Windows (#3340)
* moved db to user folder on windows

* moved db to local appdata

* made getDBFolderPath func private

* added getWindowsDbPath() func

* fix

---------

Co-authored-by: mhsanaei <ho3ein.sanaei@gmail.com>
2025-08-13 23:19:59 +02:00
mhsanaei
7258841491 Update FUNDING.yml 2025-08-12 13:00:16 +02:00
mhsanaei
23dd80fbb0 remove unnecessary ant files 2025-08-12 12:57:02 +02:00
mhsanaei
6556884c7f remove unnecessary vue files 2025-08-12 12:56:49 +02:00
Alireza Ahmadi
d5c532c64f fix saving sockopt 2025-08-09 16:07:33 +02:00
mhsanaei
ad5f774a1e Axios v1.11.0 2025-08-09 13:46:28 +02:00
g0l4
aa285914fa chore: update polygon token name (#3338) 2025-08-09 08:18:56 +02:00
mhsanaei
4d02756e1e v2.6.6 2025-08-08 20:52:04 +02:00
Alireza Ahmadi
825d93d95f upgrade telego (#3334) 2025-08-08 20:41:06 +02:00
mhsanaei
5ea6386815 better musl libc usage
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2025-08-08 19:55:24 +02:00
mhsanaei
d064e85ecd update dependencies 2025-08-08 18:56:47 +02:00
mhsanaei
9fc03bd10a remove ocspStapling 2025-08-08 18:55:52 +02:00
fgsfds
ae08a29cde fix: Xray restarting after being manually stopped (#2896) (#3329) 2025-08-07 23:35:11 +05:00
mhsanaei
4f25eb230e musl: new version 2025-08-06 23:35:20 +02:00
somebodywashere
ce72d53d1a fix: inbounds slow loading (#3228) (#3322)
especially encountered on big amount of clients
2025-08-06 15:42:32 +02:00
fgsfds
5e641ff9e8 Added Update all geofiles button (#3318)
* added Update all geofiles button

* localized update all string
2025-08-06 11:20:07 +02:00
Sanaei
58898e5758 Merge pull request #3317 from MHSanaei/dekodemo_sockopt
add sockopt to dockodemo
2025-08-05 16:51:26 +02:00
Alireza Ahmadi
569550d5f6 add sockopt to dockodemo 2025-08-05 14:02:23 +02:00
fgsfds
419ea63dd0 Added filters to the xray logs viewer (#3314)
* added filters to xray logs viewer

* better freedom/blackhole tags handling

* better freedom/blackhole tags handling 2

* fix comments

* fix comments 2
2025-08-05 12:10:14 +02:00
mhsanaei
6a17285935 remove: glibc check
now you can install on all OS like ubuntu 20 or 18
2025-08-04 19:16:56 +02:00
mhsanaei
4b03e9d919 v2.6.5 2025-08-04 19:12:37 +02:00
mhsanaei
7e9c3bdbaf fix: sub enable warning 2025-08-04 19:09:01 +02:00
fgsfds
957f3dbb54 Added xray access log viewer (#3309)
* added xray access log viewer

* made modal window width adaptive

* hide logs button if xray logs are disabled
2025-08-04 18:47:48 +02:00
mhsanaei
05e60af283 fix: IPLimitlog display 2025-08-04 18:23:37 +02:00
mhsanaei
5e40458116 fix: simplify error handling 2025-08-04 18:01:32 +02:00
Sanaei
baf6fdd29d fix portMap json (#3312)
Co-authored-by: Alireza Ahmadi <alireza7@gmail.com>
2025-08-04 17:16:11 +02:00
Sanaei
45f78d3521 Merge pull request #3311 from MHSanaei/dokodemo_portMap
add dokodemo port mapping
2025-08-04 17:01:57 +02:00
Alireza Ahmadi
01f984e054 add dokodemo port mapping 2025-08-04 16:45:09 +02:00
Sanaei
e4ba5ba53a add ech support (#3310)
Co-authored-by: Alireza Ahmadi <alireza7@gmail.com>
2025-08-04 16:27:57 +02:00
mhsanaei
6ff555c8bb runs-on: ubuntu-latest 2025-08-04 14:39:12 +02:00
elseif
3c1634ca7c use musl libc toolchains for build statically linked binaries (#3191) 2025-08-04 11:50:39 +02:00
mhsanaei
561c4810be default Max Age to 360min 2025-08-04 11:38:23 +02:00
mhsanaei
eb1b96643d update languages 2025-08-04 11:22:09 +02:00
mhsanaei
de5314c01f fix: pqv for sub #3306 2025-08-04 10:24:21 +02:00
mhsanaei
1088d1faf3 minor changes 2025-08-04 01:30:01 +02:00
mhsanaei
267024c43f xray core v25.8.3 2025-08-04 01:28:14 +02:00
mhsanaei
0d595f56e4 change a-input to a-textarea 2025-08-04 00:57:06 +02:00
fgsfds
a4c4f9efb3 kill process instead of sending SIGTERM on Windows (#3304) 2025-08-04 00:45:50 +02:00
mhsanaei
73a5722cca v2.6.3 2025-08-03 12:22:28 +02:00
mhsanaei
30264043f8 Xray core: old version removed 2025-08-03 12:22:06 +02:00
mhsanaei
c6062eb15c outbound: mldsa65Verify 2025-08-03 12:09:37 +02:00
mhsanaei
f1b7944828 pqv: mldsa65Verify 2025-08-03 12:01:49 +02:00
mhsanaei
7a57b31ff3 remove password type 2025-07-28 14:44:00 +02:00
mhsanaei
6e1b949081 Reality: min & max client ver 2025-07-28 13:45:47 +02:00
Mikhail Grigorev
0ad708b1b6 Added list of services for get public IP address (IP v4 and v6) (#3216)
* Fixed get public IP address

* Remove https://ifconfig.io/ip and https://ipinfo.tw/ip

---------

Co-authored-by: Mikhail Grigorev <grigorev_mm@magnit.ru>
2025-07-27 17:24:11 +02:00
X-Oracle
71f13ebcbd small improvement (#3277) 2025-07-27 17:22:59 +02:00
mhsanaei
f5f4a530cc xray core v25.7.26 2025-07-27 16:54:35 +02:00
Sanaei
702f03e4b7 Merge pull request #3279 from MHSanaei/mldsa65
add mldsa65
2025-07-27 16:52:44 +02:00
Alireza Ahmadi
487ec74e0b add mldsa65 2025-07-25 01:22:01 +02:00
mhsanaei
b4dae36345 xray core v25.7.24 2025-07-24 15:26:55 +02:00
mhsanaei
761728255c GO v1.24.5 + update dependencies 2025-07-24 15:17:01 +02:00
Azavax
b1ab156e42 Endpoint for updating client traffic by email (#3259)
* Update api.go

* Update inbound.go

* Update inbound.go
2025-07-22 23:43:48 +02:00
xujie86
fa45bf87de Update install.sh (#3267) 2025-07-22 23:28:56 +02:00
xujie86
75416eebd7 Increase the number of characters for webBasePath (#3239) 2025-07-22 12:53:12 +02:00
mhsanaei
87042d77ba ocspStapling set to 0 2025-07-22 12:49:02 +02:00
Danilo Nascimento
011e0f309a fix writeCrashReport() typo (#3218) 2025-07-10 02:36:02 +02:00
mhsanaei
b7164805f8 v2.6.2 2025-07-06 20:58:10 +02:00
mhsanaei
bbdeb65291 new alternative to get public IP address 2025-07-06 20:45:58 +02:00
Shishkevich D.
038cf34219 chore: return automatic generation of shadowsocks keys 2025-07-06 15:20:41 +07:00
Shishkevich D.
98a1517470 fix: login title shifts the input fields
* chore: revert "fix: reduced login title font-size for mobile (#3105)"

* chore: short login title translation for russian

* chore: change login title translation for ukrainian
2025-07-06 13:22:09 +07:00
mhsanaei
ce76cedb0d fixed: mux #3185 2025-07-04 14:02:33 +02:00
mhsanaei
24a313d605 fixed: type #3186 2025-07-04 13:25:24 +02:00
110 changed files with 3997 additions and 75871 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: MHSanaei
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username

View File

@@ -7,10 +7,10 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
submodules: true submodules: true

View File

@@ -7,6 +7,8 @@ on:
push: push:
branches: branches:
- main - main
tags:
- "v*.*.*"
paths: paths:
- '**.js' - '**.js'
- '**.css' - '**.css'
@@ -31,62 +33,48 @@ jobs:
- 386 - 386
- armv5 - armv5
- s390x - s390x
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- name: Install dependencies - name: Build 3X-UI
run: |
sudo apt-get update
if [ "${{ matrix.platform }}" == "arm64" ]; then
sudo apt install gcc-aarch64-linux-gnu
elif [ "${{ matrix.platform }}" == "armv7" ]; then
sudo apt install gcc-arm-linux-gnueabihf
elif [ "${{ matrix.platform }}" == "armv6" ]; then
sudo apt install gcc-arm-linux-gnueabihf
elif [ "${{ matrix.platform }}" == "386" ]; then
sudo apt install gcc-i686-linux-gnu
elif [ "${{ matrix.platform }}" == "armv5" ]; then
sudo apt install gcc-arm-linux-gnueabi
elif [ "${{ matrix.platform }}" == "s390x" ]; then
sudo apt install gcc-s390x-linux-gnu
fi
- name: Build 3x-ui
run: | run: |
export CGO_ENABLED=1 export CGO_ENABLED=1
export GOOS=linux export GOOS=linux
export GOARCH=${{ matrix.platform }} export GOARCH=${{ matrix.platform }}
if [ "${{ matrix.platform }}" == "arm64" ]; then # Use Bootlin prebuilt cross-toolchains (musl 1.2.5 in stable series)
export GOARCH=arm64 case "${{ matrix.platform }}" in
export CC=aarch64-linux-gnu-gcc amd64) BOOTLIN_ARCH="x86-64" ;;
elif [ "${{ matrix.platform }}" == "armv7" ]; then arm64) BOOTLIN_ARCH="aarch64" ;;
export GOARCH=arm armv7) BOOTLIN_ARCH="armv7-eabihf"; export GOARCH=arm GOARM=7 ;;
export GOARM=7 armv6) BOOTLIN_ARCH="armv6-eabihf"; export GOARCH=arm GOARM=6 ;;
export CC=arm-linux-gnueabihf-gcc armv5) BOOTLIN_ARCH="armv5-eabi"; export GOARCH=arm GOARM=5 ;;
elif [ "${{ matrix.platform }}" == "armv6" ]; then 386) BOOTLIN_ARCH="x86-i686" ;;
export GOARCH=arm s390x) BOOTLIN_ARCH="s390x-z13" ;;
export GOARM=6 esac
export CC=arm-linux-gnueabihf-gcc echo "Resolving Bootlin musl toolchain for arch=$BOOTLIN_ARCH (platform=${{ matrix.platform }})"
elif [ "${{ matrix.platform }}" == "386" ]; then TARBALL_BASE="https://toolchains.bootlin.com/downloads/releases/toolchains/$BOOTLIN_ARCH/tarballs/"
export GOARCH=386 TARBALL_URL=$(curl -fsSL "$TARBALL_BASE" | grep -oE "${BOOTLIN_ARCH}--musl--stable-[^\"]+\\.tar\\.xz" | sort -r | head -n1)
export CC=i686-linux-gnu-gcc [ -z "$TARBALL_URL" ] && { echo "Failed to locate Bootlin musl toolchain for arch=$BOOTLIN_ARCH" >&2; exit 1; }
elif [ "${{ matrix.platform }}" == "armv5" ]; then echo "Downloading: $TARBALL_URL"
export GOARCH=arm cd /tmp
export GOARM=5 curl -fL -sS -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL"
export CC=arm-linux-gnueabi-gcc tar -xf "$(basename "$TARBALL_URL")"
elif [ "${{ matrix.platform }}" == "s390x" ]; then TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1)
export GOARCH=s390x export PATH="$(realpath "$TOOLCHAIN_DIR")/bin:$PATH"
export CC=s390x-linux-gnu-gcc export CC=$(realpath "$(find "$TOOLCHAIN_DIR/bin" -name '*-gcc.br_real' -type f -executable | head -n1)")
fi [ -z "$CC" ] && { echo "No gcc.br_real found in $TOOLCHAIN_DIR/bin" >&2; exit 1; }
go build -ldflags "-w -s" -o xui-release -v main.go cd -
go build -ldflags "-w -s -linkmode external -extldflags '-static'" -o xui-release -v main.go
file xui-release
ldd xui-release || echo "Static binary confirmed"
mkdir x-ui mkdir x-ui
cp xui-release x-ui/ cp xui-release x-ui/
@@ -97,7 +85,7 @@ jobs:
cd x-ui/bin cd x-ui/bin
# Download dependencies # Download dependencies
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.6.8/" Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.9.11/"
if [ "${{ matrix.platform }}" == "amd64" ]; then if [ "${{ matrix.platform }}" == "amd64" ]; then
wget -q ${Xray_URL}Xray-linux-64.zip wget -q ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip unzip Xray-linux-64.zip
@@ -148,10 +136,89 @@ jobs:
- name: Upload files to GH release - name: Upload files to GH release
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2
if: github.event_name == 'release' && github.event.action == 'published' if: |
(github.event_name == 'release' && github.event.action == 'published') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }} tag: ${{ github.ref }}
file: x-ui-linux-${{ matrix.platform }}.tar.gz file: x-ui-linux-${{ matrix.platform }}.tar.gz
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
overwrite: true
prerelease: true prerelease: true
# =================================
# Windows Build
# =================================
build-windows:
name: Build for Windows
permissions:
contents: write
strategy:
matrix:
platform:
- amd64
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Build 3X-UI for Windows
shell: pwsh
run: |
$env:CGO_ENABLED="1"
$env:GOOS="windows"
$env:GOARCH="amd64"
go build -ldflags "-w -s" -o xui-release.exe -v main.go
mkdir x-ui
Copy-Item xui-release.exe x-ui\
mkdir x-ui\bin
cd x-ui\bin
# Download Xray for Windows
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.9.11/"
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
Remove-Item "Xray-windows-64.zip"
Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat"
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat"
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat"
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat"
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat"
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat"
Rename-Item xray.exe xray-windows-amd64.exe
cd ..
Copy-Item -Path ..\windows_files\* -Destination . -Recurse
cd ..
- name: Package to Zip
shell: pwsh
run: |
Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
- name: Upload files to Artifacts
uses: actions/upload-artifact@v4
with:
name: x-ui-windows-amd64
path: ./x-ui-windows-amd64.zip
- name: Upload files to GH release
uses: svenstaro/upload-release-action@v2
if: |
(github.event_name == 'release' && github.event.action == 'published') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-windows-amd64.zip
asset_name: x-ui-windows-amd64.zip
overwrite: true
prerelease: true

4
.gitignore vendored
View File

@@ -29,9 +29,9 @@ main
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Ignore Go specific files # Ignore Go build files
*.exe *.exe
*.exe~ x-ui.db
# Ignore Docker specific files # Ignore Docker specific files
docker-compose.override.yml docker-compose.override.yml

View File

@@ -27,7 +27,7 @@ case $1 in
esac esac
mkdir -p build/bin mkdir -p build/bin
cd build/bin cd build/bin
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.6.8/Xray-linux-${ARCH}.zip" wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.9.11/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip" unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "xray-linux-${FNAME}" mv xray "xray-linux-${FNAME}"

View File

@@ -1,7 +1,7 @@
# ======================================================== # ========================================================
# Stage: Builder # Stage: Builder
# ======================================================== # ========================================================
FROM golang:1.24-alpine AS builder FROM golang:1.25-alpine AS builder
WORKDIR /app WORKDIR /app
ARG TARGETARCH ARG TARGETARCH

View File

@@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
</p> </p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## النجوم عبر الزمن ## النجوم عبر الزمن

View File

@@ -48,7 +48,7 @@ Para documentación completa, visita la [Wiki del proyecto](https://github.com/M
</p> </p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## Estrellas a lo Largo del Tiempo ## Estrellas a lo Largo del Tiempo

View File

@@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
</p> </p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## ستاره‌ها در طول زمان ## ستاره‌ها در طول زمان

View File

@@ -48,7 +48,7 @@ For full documentation, please visit the [project Wiki](https://github.com/MHSan
</p> </p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## Stargazers over Time ## Stargazers over Time

View File

@@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
</p> </p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## Звезды с течением времени ## Звезды с течением времени

View File

@@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
</p> </p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## 随时间变化的星标数 ## 随时间变化的星标数

View File

@@ -3,7 +3,10 @@ package config
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath"
"runtime"
"strings" "strings"
) )
@@ -54,12 +57,32 @@ func GetBinFolderPath() string {
return binFolderPath return binFolderPath
} }
func getBaseDir() string {
exePath, err := os.Executable()
if err != nil {
return "."
}
exeDir := filepath.Dir(exePath)
exeDirLower := strings.ToLower(filepath.ToSlash(exeDir))
if strings.Contains(exeDirLower, "/appdata/local/temp/") || strings.Contains(exeDirLower, "/go-build") {
wd, err := os.Getwd()
if err != nil {
return "."
}
return wd
}
return exeDir
}
func GetDBFolderPath() string { func GetDBFolderPath() string {
dbFolderPath := os.Getenv("XUI_DB_FOLDER") dbFolderPath := os.Getenv("XUI_DB_FOLDER")
if dbFolderPath == "" { if dbFolderPath != "" {
dbFolderPath = "/etc/x-ui" return dbFolderPath
} }
return dbFolderPath if runtime.GOOS == "windows" {
return getBaseDir()
}
return "/etc/x-ui"
} }
func GetDBPath() string { func GetDBPath() string {
@@ -68,8 +91,54 @@ func GetDBPath() string {
func GetLogFolder() string { func GetLogFolder() string {
logFolderPath := os.Getenv("XUI_LOG_FOLDER") logFolderPath := os.Getenv("XUI_LOG_FOLDER")
if logFolderPath == "" { if logFolderPath != "" {
logFolderPath = "/var/log" return logFolderPath
} }
return logFolderPath if runtime.GOOS == "windows" {
return filepath.Join(".", "log")
}
return "/var/log"
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
if err != nil {
return err
}
return out.Sync()
}
func init() {
if runtime.GOOS != "windows" {
return
}
if os.Getenv("XUI_DB_FOLDER") != "" {
return
}
oldDBFolder := "/etc/x-ui"
oldDBPath := fmt.Sprintf("%s/%s.db", oldDBFolder, GetName())
newDBFolder := GetDBFolderPath()
newDBPath := fmt.Sprintf("%s/%s.db", newDBFolder, GetName())
_, err := os.Stat(newDBPath)
if err == nil {
return // new exists
}
_, err = os.Stat(oldDBPath)
if os.IsNotExist(err) {
return // old does not exist
}
_ = copyFile(oldDBPath, newDBPath) // ignore error
} }

View File

@@ -1 +1 @@
2.6.1 2.8.0

View File

@@ -12,11 +12,11 @@ type Protocol string
const ( const (
VMESS Protocol = "vmess" VMESS Protocol = "vmess"
VLESS Protocol = "vless" VLESS Protocol = "vless"
DOKODEMO Protocol = "dokodemo-door" Tunnel Protocol = "tunnel"
HTTP Protocol = "http" HTTP Protocol = "http"
Trojan Protocol = "trojan" Trojan Protocol = "trojan"
Shadowsocks Protocol = "shadowsocks" Shadowsocks Protocol = "shadowsocks"
Socks Protocol = "socks" Mixed Protocol = "mixed"
WireGuard Protocol = "wireguard" WireGuard Protocol = "wireguard"
) )
@@ -32,6 +32,7 @@ type Inbound struct {
Up int64 `json:"up" form:"up"` Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"` Down int64 `json:"down" form:"down"`
Total int64 `json:"total" form:"total"` Total int64 `json:"total" form:"total"`
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"`
Remark string `json:"remark" form:"remark"` Remark string `json:"remark" form:"remark"`
Enable bool `json:"enable" form:"enable"` Enable bool `json:"enable" form:"enable"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
@@ -45,7 +46,6 @@ type Inbound struct {
StreamSettings string `json:"streamSettings" form:"streamSettings"` StreamSettings string `json:"streamSettings" form:"streamSettings"`
Tag string `json:"tag" form:"tag" gorm:"unique"` Tag string `json:"tag" form:"tag" gorm:"unique"`
Sniffing string `json:"sniffing" form:"sniffing"` Sniffing string `json:"sniffing" form:"sniffing"`
Allocate string `json:"allocate" form:"allocate"`
} }
type OutboundTraffics struct { type OutboundTraffics struct {
@@ -80,7 +80,6 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
StreamSettings: json_util.RawMessage(i.StreamSettings), StreamSettings: json_util.RawMessage(i.StreamSettings),
Tag: i.Tag, Tag: i.Tag,
Sniffing: json_util.RawMessage(i.Sniffing), Sniffing: json_util.RawMessage(i.Sniffing),
Allocate: json_util.RawMessage(i.Allocate),
} }
} }
@@ -104,4 +103,13 @@ type Client struct {
SubID string `json:"subId" form:"subId"` SubID string `json:"subId" form:"subId"`
Comment string `json:"comment" form:"comment"` Comment string `json:"comment" form:"comment"`
Reset int `json:"reset" form:"reset"` Reset int `json:"reset" form:"reset"`
CreatedAt int64 `json:"created_at,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
}
type VLESSSettings struct {
Clients []Client `json:"clients"`
Decryption string `json:"decryption"`
Encryption string `json:"encryption"`
Fallbacks []any `json:"fallbacks"`
} }

76
go.mod
View File

@@ -1,6 +1,6 @@
module x-ui module x-ui
go 1.24.4 go 1.25.1
require ( require (
github.com/gin-contrib/gzip v1.2.3 github.com/gin-contrib/gzip v1.2.3
@@ -9,41 +9,40 @@ require (
github.com/goccy/go-json v0.10.5 github.com/goccy/go-json v0.10.5
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mymmrac/telego v0.32.0 github.com/mymmrac/telego v1.3.0
github.com/nicksnyder/go-i18n/v2 v2.6.0 github.com/nicksnyder/go-i18n/v2 v2.6.0
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.6 github.com/shirou/gopsutil/v4 v4.25.8
github.com/valyala/fasthttp v1.63.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/valyala/fasthttp v1.65.0
github.com/xlzd/gotp v0.1.0 github.com/xlzd/gotp v0.1.0
github.com/xtls/xray-core v1.250608.0 github.com/xtls/xray-core v1.250911.0
go.uber.org/atomic v1.11.0 go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.39.0 golang.org/x/crypto v0.42.0
golang.org/x/text v0.26.0 golang.org/x/text v0.29.0
google.golang.org/grpc v1.73.0 google.golang.org/grpc v1.75.1
gorm.io/driver/sqlite v1.6.0 gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.30.0 gorm.io/gorm v1.30.5
) )
require ( require (
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/sonic v1.13.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
github.com/ebitengine/purego v0.8.4 // indirect github.com/ebitengine/purego v0.8.4 // indirect
github.com/fasthttp/router v1.5.4 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ole/go-ole v1.3.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/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect
github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect github.com/gorilla/sessions v1.4.0 // indirect
@@ -54,24 +53,24 @@ require (
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/ratelimit v1.0.2 // indirect github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // 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/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.52.0 // indirect github.com/quic-go/quic-go v0.54.0 // indirect
github.com/refraction-networking/utls v1.7.3 // indirect github.com/refraction-networking/utls v1.8.0 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagernet/sing v0.6.6 // indirect github.com/sagernet/sing v0.7.7 // indirect
github.com/sagernet/sing-shadowsocks v0.2.7 // indirect github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect github.com/tklauser/numcpus v0.10.0 // indirect
@@ -82,23 +81,22 @@ require (
github.com/valyala/fastjson v1.6.4 // indirect github.com/valyala/fastjson v1.6.4 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/xtls/reality v0.0.0-20250627141458-e62c4aed0d57 // indirect github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/mock v0.6.0 // indirect
go.uber.org/mock v0.5.2 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.18.0 // indirect golang.org/x/arch v0.21.0 // indirect
golang.org/x/mod v0.25.0 // indirect golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.41.0 // indirect golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.15.0 // indirect golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.36.0 // indirect
golang.org/x/time v0.12.0 // indirect golang.org/x/time v0.13.0 // indirect
golang.org/x/tools v0.34.0 // indirect golang.org/x/tools v0.36.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 // indirect gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
lukechampine.com/blake3 v1.4.1 // indirect lukechampine.com/blake3 v1.4.1 // indirect
) )

191
go.sum
View File

@@ -1,19 +1,18 @@
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 h1:Wo41lDOevRJSGpevP+8Pk5bANX7fJacO2w04aqLiC5I=
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -22,10 +21,8 @@ github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mT
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fasthttp/router v1.5.4 h1:oxdThbBwQgsDIYZ3wR1IavsNl6ZS9WdjKukeMikOnC8= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/fasthttp/router v1.5.4/go.mod h1:3/hysWq6cky7dTfzaaEPZGdptwjwx0qzTgFCKEWRjgc= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U= github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U=
@@ -36,8 +33,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
@@ -51,8 +48,6 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
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.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 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/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 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
@@ -66,8 +61,6 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 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/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ=
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
@@ -92,37 +85,31 @@ github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 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/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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.2.11/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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v0.32.0 h1:4X8C1l3k+opkk86r95+eQE8DxiS2LYlR61L/G7yreDY= github.com/mymmrac/telego v1.3.0 h1:y2bDDCioLgkcs+5luUaPgTNHKel1Qh30iUxFcMUrowg=
github.com/mymmrac/telego v0.32.0/go.mod h1:qS6NaRhJgcuEEBEMVCV79S2xCAuHq9O+ixwfLuRW31M= github.com/mymmrac/telego v1.3.0/go.mod h1:0D2l/IA/gUFn4oqsi1O4/tSnlezw5jNV/ReFRDUEKk8=
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ= github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE= github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
@@ -135,41 +122,38 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= 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/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/refraction-networking/utls v1.7.3 h1:L0WRhHY7Oq1T0zkdzVZMR6zWZv+sXbHB9zcuvsAEqCo= github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
github.com/refraction-networking/utls v1.7.3/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ= github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 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/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagernet/sing v0.6.6 h1:3JkvJ0vqDj/jJcx0a+ve/6lMOrSzZm30I3wrIuZtmRE= github.com/sagernet/sing v0.7.7 h1:o46FzVZS+wKbBMEkMEdEHoVZxyM9jvfRpKXc7pEgS/c=
github.com/sagernet/sing v0.6.6/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.7.7/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 h1:qIQ0tWF9vxGtkJa24bR+2i53WBCz1nW/Pc47oVYauC4=
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287/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 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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.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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= 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/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 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
@@ -182,8 +166,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/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.63.0 h1:DisIL8OjB7ul2d7cBaMRcKTQDYnrGy56R4FCiuDP0Ns= github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
github.com/valyala/fasthttp v1.63.0/go.mod h1:REc4IeW+cAEyLrRPa5A81MIjvz0QE1laoTX2EaPHKJM= github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
@@ -192,68 +176,68 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po= github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
github.com/xtls/reality v0.0.0-20250627141458-e62c4aed0d57 h1:CJzC54UytAYnNbJSlAFi9MXOofSUAtpoQTKIA3hUpj8= github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c h1:LHLhQY3mKXSpTcQAkjFR4/6ar3rXjQryNeM7khK3AHU=
github.com/xtls/reality v0.0.0-20250627141458-e62c4aed0d57/go.mod h1:yD47RN65bDLZgyHWMfFDiqlzrq4usDMt/Xzsk6tMbhw= github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
github.com/xtls/xray-core v1.250608.0 h1:/M0LwzFeFAZf+vdZQhqNYjUXDfPv5hOeYw6jsiAdgWI= github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno=
github.com/xtls/xray-core v1.250608.0/go.mod h1:MkfIs2WZ5VLtZHAwDKosSS05Kx5zFFOzvly7Hy6pfPs= github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 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/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= 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= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.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.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= 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/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-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -265,10 +249,9 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h1:sfK5nHuG7lRFZ2FdTT3RimOqWBg8IrVm+/Vko1FVOsk= gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@@ -39,19 +39,6 @@ arch() {
echo "Arch: $(arch)" echo "Arch: $(arch)"
check_glibc_version() {
glibc_version=$(ldd --version | head -n1 | awk '{print $NF}')
required_version="2.32"
if [[ "$(printf '%s\n' "$required_version" "$glibc_version" | sort -V | head -n1)" != "$required_version" ]]; then
echo -e "${red}GLIBC version $glibc_version is too old! Required: 2.32 or higher${plain}"
echo "Please upgrade to a newer version of your operating system to get a higher GLIBC version."
exit 1
fi
echo "GLIBC version: $glibc_version (meets requirement of 2.32+)"
}
check_glibc_version
install_base() { install_base() {
case "${release}" in case "${release}" in
ubuntu | debian | armbian) ubuntu | debian | armbian)
@@ -70,7 +57,7 @@ install_base() {
zypper refresh && zypper -q install -y wget curl tar timezone zypper refresh && zypper -q install -y wget curl tar timezone
;; ;;
*) *)
apt-get update && apt install -y -q wget curl tar tzdata apt-get update && apt-get install -y -q wget curl tar tzdata
;; ;;
esac esac
} }
@@ -85,11 +72,25 @@ config_after_install() {
local existing_hasDefaultCredential=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}') local existing_hasDefaultCredential=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}')
local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
local server_ip=$(curl -s https://api.ipify.org) local URL_lists=(
"https://api4.ipify.org"
"https://ipv4.icanhazip.com"
"https://v4.api.ipinfo.io/ip"
"https://ipv4.myexternalip.com/raw"
"https://4.ident.me"
"https://check-host.net/ip"
)
local server_ip=""
for ip_address in "${URL_lists[@]}"; do
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
if [[ -n "${server_ip}" ]]; then
break
fi
done
if [[ ${#existing_webBasePath} -lt 4 ]]; then if [[ ${#existing_webBasePath} -lt 4 ]]; then
if [[ "$existing_hasDefaultCredential" == "true" ]]; then if [[ "$existing_hasDefaultCredential" == "true" ]]; then
local config_webBasePath=$(gen_random_string 15) local config_webBasePath=$(gen_random_string 18)
local config_username=$(gen_random_string 10) local config_username=$(gen_random_string 10)
local config_password=$(gen_random_string 10) local config_password=$(gen_random_string 10)
@@ -112,7 +113,7 @@ config_after_install() {
echo -e "${green}Access URL: http://${server_ip}:${config_port}/${config_webBasePath}${plain}" echo -e "${green}Access URL: http://${server_ip}:${config_port}/${config_webBasePath}${plain}"
echo -e "###############################################" echo -e "###############################################"
else else
local config_webBasePath=$(gen_random_string 15) local config_webBasePath=$(gen_random_string 18)
echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}" echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}"
/usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}" /usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}"
echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}" echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
@@ -141,6 +142,7 @@ config_after_install() {
install_x-ui() { install_x-ui() {
cd /usr/local/ cd /usr/local/
# Download resources
if [ $# == 0 ]; then if [ $# == 0 ]; then
tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$tag_version" ]]; then if [[ ! -n "$tag_version" ]]; then
@@ -171,30 +173,35 @@ install_x-ui() {
exit 1 exit 1
fi fi
fi fi
wget -O /usr/bin/x-ui-temp https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
# Stop x-ui service and remove old resources
if [[ -e /usr/local/x-ui/ ]]; then if [[ -e /usr/local/x-ui/ ]]; then
systemctl stop x-ui systemctl stop x-ui
rm /usr/local/x-ui/ -rf rm /usr/local/x-ui/ -rf
fi fi
# Extract resources and set permissions
tar zxvf x-ui-linux-$(arch).tar.gz tar zxvf x-ui-linux-$(arch).tar.gz
rm x-ui-linux-$(arch).tar.gz -f rm x-ui-linux-$(arch).tar.gz -f
cd x-ui cd x-ui
chmod +x x-ui chmod +x x-ui
chmod +x x-ui.sh
# Check the system's architecture and rename the file accordingly # Check the system's architecture and rename the file accordingly
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
mv bin/xray-linux-$(arch) bin/xray-linux-arm mv bin/xray-linux-$(arch) bin/xray-linux-arm
chmod +x bin/xray-linux-arm chmod +x bin/xray-linux-arm
fi fi
chmod +x x-ui bin/xray-linux-$(arch) chmod +x x-ui bin/xray-linux-$(arch)
cp -f x-ui.service /etc/systemd/system/
wget -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh # Update x-ui cli and se set permission
chmod +x /usr/local/x-ui/x-ui.sh mv -f /usr/bin/x-ui-temp /usr/bin/x-ui
chmod +x /usr/bin/x-ui chmod +x /usr/bin/x-ui
config_after_install config_after_install
cp -f x-ui.service /etc/systemd/system/
systemctl daemon-reload systemctl daemon-reload
systemctl enable x-ui systemctl enable x-ui
systemctl start x-ui systemctl start x-ui

View File

@@ -13,7 +13,7 @@
"inbounds": [ "inbounds": [
{ {
"port": 10808, "port": 10808,
"protocol": "socks", "protocol": "mixed",
"settings": { "settings": {
"auth": "noauth", "auth": "noauth",
"udp": true, "udp": true,
@@ -28,7 +28,7 @@
], ],
"enabled": true "enabled": true
}, },
"tag": "socks" "tag": "mixed"
}, },
{ {
"port": 10809, "port": 10809,

View File

@@ -3,14 +3,19 @@ package sub
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"html/template"
"io" "io"
"io/fs"
"net" "net"
"net/http" "net/http"
"os"
"path/filepath"
"strconv" "strconv"
"x-ui/config"
"x-ui/logger" "x-ui/logger"
"x-ui/util/common" "x-ui/util/common"
webpkg "x-ui/web"
"x-ui/web/locale"
"x-ui/web/middleware" "x-ui/web/middleware"
"x-ui/web/network" "x-ui/web/network"
"x-ui/web/service" "x-ui/web/service"
@@ -18,6 +23,21 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// setEmbeddedTemplates parses and sets embedded templates on the engine
func setEmbeddedTemplates(engine *gin.Engine) error {
t, err := template.New("").Funcs(engine.FuncMap).ParseFS(
webpkg.EmbeddedHTML(),
"html/common/page.html",
"html/component/aThemeSwitch.html",
"html/subscription.html",
)
if err != nil {
return err
}
engine.SetHTMLTemplate(t)
return nil
}
type Server struct { type Server struct {
httpServer *http.Server httpServer *http.Server
listener net.Listener listener net.Listener
@@ -38,13 +58,10 @@ func NewServer() *Server {
} }
func (s *Server) initRouter() (*gin.Engine, error) { func (s *Server) initRouter() (*gin.Engine, error) {
if config.IsDebug() { // Always run in release mode for the subscription server
gin.SetMode(gin.DebugMode) gin.DefaultWriter = io.Discard
} else { gin.DefaultErrorWriter = io.Discard
gin.DefaultWriter = io.Discard gin.SetMode(gin.ReleaseMode)
gin.DefaultErrorWriter = io.Discard
gin.SetMode(gin.ReleaseMode)
}
engine := gin.Default() engine := gin.Default()
@@ -57,6 +74,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine.Use(middleware.DomainValidatorMiddleware(subDomain)) engine.Use(middleware.DomainValidatorMiddleware(subDomain))
} }
// Provide base_path in context for templates
engine.Use(func(c *gin.Context) {
c.Set("base_path", "/")
})
LinksPath, err := s.settingService.GetSubPath() LinksPath, err := s.settingService.GetSubPath()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -112,6 +134,36 @@ func (s *Server) initRouter() (*gin.Engine, error) {
SubTitle = "" SubTitle = ""
} }
// set per-request localizer from headers/cookies
engine.Use(locale.LocalizerMiddleware())
// register i18n function similar to web server
i18nWebFunc := func(key string, params ...string) string {
return locale.I18n(locale.Web, key, params...)
}
engine.SetFuncMap(map[string]any{"i18n": i18nWebFunc})
// Templates: prefer embedded; fallback to disk if necessary
if err := setEmbeddedTemplates(engine); err != nil {
logger.Warning("sub: failed to parse embedded templates:", err)
if files, derr := s.getHtmlFiles(); derr == nil {
engine.LoadHTMLFiles(files...)
} else {
logger.Error("sub: no templates available (embedded parse and disk load failed)", err, derr)
}
}
// Assets: use disk if present, fallback to embedded
if _, err := os.Stat("web/assets"); err == nil {
engine.StaticFS("/assets", http.FS(os.DirFS("web/assets")))
} else {
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
engine.StaticFS("/assets", http.FS(subFS))
} else {
logger.Error("sub: failed to mount embedded assets:", err)
}
}
g := engine.Group("/") g := engine.Group("/")
s.sub = NewSUBController( s.sub = NewSUBController(
@@ -121,6 +173,30 @@ func (s *Server) initRouter() (*gin.Engine, error) {
return engine, nil return engine, nil
} }
// getHtmlFiles loads templates from local folder (used in debug mode)
func (s *Server) getHtmlFiles() ([]string, error) {
dir, _ := os.Getwd()
files := []string{}
// common layout
common := filepath.Join(dir, "web", "html", "common", "page.html")
if _, err := os.Stat(common); err == nil {
files = append(files, common)
}
// components used
theme := filepath.Join(dir, "web", "html", "component", "aThemeSwitch.html")
if _, err := os.Stat(theme); err == nil {
files = append(files, theme)
}
// page itself
page := filepath.Join(dir, "web", "html", "subscription.html")
if _, err := os.Stat(page); err == nil {
files = append(files, page)
} else {
return nil, err
}
return files, nil
}
func (s *Server) Start() (err error) { func (s *Server) Start() (err error) {
// This is an anonymous function, no function name // This is an anonymous function, no function name
defer func() { defer func() {

View File

@@ -2,8 +2,8 @@ package sub
import ( import (
"encoding/base64" "encoding/base64"
"net"
"strings" "strings"
"x-ui/config"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -53,27 +53,13 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
gJson := g.Group(a.subJsonPath) gJson := g.Group(a.subJsonPath)
gLink.GET(":subid", a.subs) gLink.GET(":subid", a.subs)
gJson.GET(":subid", a.subJsons) gJson.GET(":subid", a.subJsons)
} }
func (a *SUBController) subs(c *gin.Context) { func (a *SUBController) subs(c *gin.Context) {
subId := c.Param("subid") subId := c.Param("subid")
var host string scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c)
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil { subs, header, lastOnline, err := a.subService.GetSubs(subId, host)
host = h
}
if host == "" {
host = c.GetHeader("X-Real-IP")
}
if host == "" {
var err error
host, _, err = net.SplitHostPort(c.Request.Host)
if err != nil {
host = c.Request.Host
}
}
subs, header, err := a.subService.GetSubs(subId, host)
if err != nil || len(subs) == 0 { if err != nil || len(subs) == 0 {
c.String(400, "Error!") c.String(400, "Error!")
} else { } else {
@@ -82,10 +68,38 @@ func (a *SUBController) subs(c *gin.Context) {
result += sub + "\n" result += sub + "\n"
} }
// If the request expects HTML (e.g., browser) or explicitly asked (?html=1 or ?view=html), render the info page here
accept := c.GetHeader("Accept")
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
// Build page data in service
subURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId)
page := a.subService.BuildPageData(subId, hostHeader, header, lastOnline, subs, subURL, subJsonURL)
c.HTML(200, "subscription.html", gin.H{
"title": "subscription.title",
"cur_ver": config.GetVersion(),
"host": page.Host,
"base_path": page.BasePath,
"sId": page.SId,
"download": page.Download,
"upload": page.Upload,
"total": page.Total,
"used": page.Used,
"remained": page.Remained,
"expire": page.Expire,
"lastOnline": page.LastOnline,
"datepicker": page.Datepicker,
"downloadByte": page.DownloadByte,
"uploadByte": page.UploadByte,
"totalByte": page.TotalByte,
"subUrl": page.SubUrl,
"subJsonUrl": page.SubJsonUrl,
"result": page.Result,
})
return
}
// Add headers // Add headers
c.Writer.Header().Set("Subscription-Userinfo", header) a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
c.Writer.Header().Set("Profile-Title", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
if a.subEncrypt { if a.subEncrypt {
c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
@@ -97,41 +111,21 @@ func (a *SUBController) subs(c *gin.Context) {
func (a *SUBController) subJsons(c *gin.Context) { func (a *SUBController) subJsons(c *gin.Context) {
subId := c.Param("subid") subId := c.Param("subid")
var host string _, host, _, _ := a.subService.ResolveRequest(c)
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil {
host = h
}
if host == "" {
host = c.GetHeader("X-Real-IP")
}
if host == "" {
var err error
host, _, err = net.SplitHostPort(c.Request.Host)
if err != nil {
host = c.Request.Host
}
}
jsonSub, header, err := a.subJsonService.GetJson(subId, host) jsonSub, header, err := a.subJsonService.GetJson(subId, host)
if err != nil || len(jsonSub) == 0 { if err != nil || len(jsonSub) == 0 {
c.String(400, "Error!") c.String(400, "Error!")
} else { } else {
// Add headers // Add headers
c.Writer.Header().Set("Subscription-Userinfo", header) a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
c.Writer.Header().Set("Profile-Title", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
c.String(200, jsonSub) c.String(200, jsonSub)
} }
} }
func getHostFromXFH(s string) (string, error) { func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
if strings.Contains(s, ":") { c.Writer.Header().Set("Subscription-Userinfo", header)
realHost, _, err := net.SplitHostPort(s) c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
if err != nil { c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle)))
return "", err
}
return realHost, nil
}
return s, nil
} }

View File

@@ -184,8 +184,14 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
var newOutbounds []json_util.RawMessage var newOutbounds []json_util.RawMessage
switch inbound.Protocol { switch inbound.Protocol {
case "vmess", "vless": case "vmess":
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client)) newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client, ""))
case "vless":
var vlessSettings model.VLESSSettings
_ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings)
newOutbounds = append(newOutbounds,
s.genVnext(inbound, streamSettings, client, vlessSettings.Encryption))
case "trojan", "shadowsocks": case "trojan", "shadowsocks":
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client)) newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
} }
@@ -209,9 +215,10 @@ func (s *SubJsonService) streamData(stream string) map[string]any {
var streamSettings map[string]any var streamSettings map[string]any
json.Unmarshal([]byte(stream), &streamSettings) json.Unmarshal([]byte(stream), &streamSettings)
security, _ := streamSettings["security"].(string) security, _ := streamSettings["security"].(string)
if security == "tls" { switch security {
case "tls":
streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any)) streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any))
} else if security == "reality" { case "reality":
streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any)) streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any))
} }
delete(streamSettings, "sockopt") delete(streamSettings, "sockopt")
@@ -263,6 +270,7 @@ func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
rltyData["show"] = false rltyData["show"] = false
rltyData["publicKey"] = rltyClientSettings["publicKey"] rltyData["publicKey"] = rltyClientSettings["publicKey"]
rltyData["fingerprint"] = rltyClientSettings["fingerprint"] rltyData["fingerprint"] = rltyClientSettings["fingerprint"]
rltyData["mldsa65Verify"] = rltyClientSettings["mldsa65Verify"]
// Set random data // Set random data
rltyData["spiderX"] = "/" + random.Seq(15) rltyData["spiderX"] = "/" + random.Seq(15)
@@ -282,7 +290,7 @@ func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
return rltyData return rltyData
} }
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage { func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, encryption string) json_util.RawMessage {
outbound := Outbound{} outbound := Outbound{}
usersData := make([]UserVnext, 1) usersData := make([]UserVnext, 1)
@@ -293,7 +301,7 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
} }
if inbound.Protocol == model.VLESS { if inbound.Protocol == model.VLESS {
usersData[0].Flow = client.Flow usersData[0].Flow = client.Flow
usersData[0].Encryption = "none" usersData[0].Encryption = encryption
} }
vnextData := make([]VnextSetting, 1) vnextData := make([]VnextSetting, 1)

View File

@@ -3,10 +3,15 @@ package sub
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"net"
"net/url" "net/url"
"strconv"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/goccy/go-json"
"x-ui/database" "x-ui/database"
"x-ui/database/model" "x-ui/database/model"
"x-ui/logger" "x-ui/logger"
@@ -14,8 +19,6 @@ import (
"x-ui/util/random" "x-ui/util/random"
"x-ui/web/service" "x-ui/web/service"
"x-ui/xray" "x-ui/xray"
"github.com/goccy/go-json"
) )
type SubService struct { type SubService struct {
@@ -34,19 +37,20 @@ func NewSubService(showInfo bool, remarkModel string) *SubService {
} }
} }
func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) { func (s *SubService) GetSubs(subId string, host string) ([]string, string, int64, error) {
s.address = host s.address = host
var result []string var result []string
var header string var header string
var traffic xray.ClientTraffic var traffic xray.ClientTraffic
var lastOnline int64
var clientTraffics []xray.ClientTraffic var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId) inbounds, err := s.getInboundsBySubId(subId)
if err != nil { if err != nil {
return nil, "", err return nil, "", 0, err
} }
if len(inbounds) == 0 { if len(inbounds) == 0 {
return nil, "", common.NewError("No inbounds found with ", subId) return nil, "", 0, common.NewError("No inbounds found with ", subId)
} }
s.datepicker, err = s.settingService.GetDatepicker() s.datepicker, err = s.settingService.GetDatepicker()
@@ -73,7 +77,11 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
if client.Enable && client.SubID == subId { if client.Enable && client.SubID == subId {
link := s.getLink(inbound, client.Email) link := s.getLink(inbound, client.Email)
result = append(result, link) result = append(result, link)
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email)) ct := s.getClientTraffics(inbound.ClientStats, client.Email)
clientTraffics = append(clientTraffics, ct)
if ct.LastOnline > lastOnline {
lastOnline = ct.LastOnline
}
} }
} }
} }
@@ -101,7 +109,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
} }
} }
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
return result, header, nil return result, header, lastOnline, nil
} }
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) { func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
@@ -313,6 +321,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
if inbound.Protocol != model.VLESS { if inbound.Protocol != model.VLESS {
return "" return ""
} }
var vlessSettings model.VLESSSettings
_ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings)
var stream map[string]any var stream map[string]any
json.Unmarshal([]byte(inbound.StreamSettings), &stream) json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.GetClients(inbound) clients, _ := s.inboundService.GetClients(inbound)
@@ -327,6 +338,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
port := inbound.Port port := inbound.Port
streamNetwork := stream["network"].(string) streamNetwork := stream["network"].(string)
params := make(map[string]string) params := make(map[string]string)
if vlessSettings.Encryption != "" {
params["encryption"] = vlessSettings.Encryption
}
params["type"] = streamNetwork params["type"] = streamNetwork
switch streamNetwork { switch streamNetwork {
@@ -437,6 +451,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["fp"] = fp params["fp"] = fp
} }
} }
if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok {
if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 {
params["pqv"] = pqv
}
}
params["spx"] = "/" + random.Seq(15) params["spx"] = "/" + random.Seq(15)
} }
@@ -627,6 +646,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["fp"] = fp params["fp"] = fp
} }
} }
if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok {
if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 {
params["pqv"] = pqv
}
}
params["spx"] = "/" + random.Seq(15) params["spx"] = "/" + random.Seq(15)
} }
@@ -985,3 +1009,172 @@ func searchHost(headers any) string {
return "" return ""
} }
// PageData is a view model for subscription.html
type PageData struct {
Host string
BasePath string
SId string
Download string
Upload string
Total string
Used string
Remained string
Expire int64
LastOnline int64
Datepicker string
DownloadByte int64
UploadByte int64
TotalByte int64
SubUrl string
SubJsonUrl string
Result []string
}
// ResolveRequest extracts scheme and host info from request/headers consistently.
func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) {
// scheme
scheme = "http"
if c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") {
scheme = "https"
}
// base host (no port)
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil && h != "" {
host = h
}
if host == "" {
host = c.GetHeader("X-Real-IP")
}
if host == "" {
var err error
host, _, err = net.SplitHostPort(c.Request.Host)
if err != nil {
host = c.Request.Host
}
}
// host:port for URLs
hostWithPort = c.GetHeader("X-Forwarded-Host")
if hostWithPort == "" {
hostWithPort = c.Request.Host
}
if hostWithPort == "" {
hostWithPort = host
}
// header display host
hostHeader = c.GetHeader("X-Forwarded-Host")
if hostHeader == "" {
hostHeader = c.GetHeader("X-Real-IP")
}
if hostHeader == "" {
hostHeader = host
}
return
}
// BuildURLs constructs absolute subscription and json URLs.
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) {
if strings.HasSuffix(subPath, "/") {
subURL = scheme + "://" + hostWithPort + subPath + subId
} else {
subURL = scheme + "://" + hostWithPort + strings.TrimRight(subPath, "/") + "/" + subId
}
if strings.HasSuffix(subJsonPath, "/") {
subJsonURL = scheme + "://" + hostWithPort + subJsonPath + subId
} else {
subJsonURL = scheme + "://" + hostWithPort + strings.TrimRight(subJsonPath, "/") + "/" + subId
}
return
}
// BuildPageData parses header and prepares the template view model.
func (s *SubService) BuildPageData(subId, hostHeader, header string, lastOnline int64, subs []string, subURL, subJsonURL string) PageData {
// Parse header values
var uploadByte, downloadByte, totalByte, expire int64
parts := strings.Split(header, ";")
for _, p := range parts {
kv := strings.Split(strings.TrimSpace(p), "=")
if len(kv) != 2 {
continue
}
key := strings.ToLower(strings.TrimSpace(kv[0]))
val := strings.TrimSpace(kv[1])
switch key {
case "upload":
if v, err := parseInt64(val); err == nil {
uploadByte = v
}
case "download":
if v, err := parseInt64(val); err == nil {
downloadByte = v
}
case "total":
if v, err := parseInt64(val); err == nil {
totalByte = v
}
case "expire":
if v, err := parseInt64(val); err == nil {
expire = v
}
}
}
download := common.FormatTraffic(downloadByte)
upload := common.FormatTraffic(uploadByte)
total := "∞"
used := common.FormatTraffic(uploadByte + downloadByte)
remained := ""
if totalByte > 0 {
total = common.FormatTraffic(totalByte)
left := totalByte - (uploadByte + downloadByte)
if left < 0 {
left = 0
}
remained = common.FormatTraffic(left)
}
datepicker := s.datepicker
if datepicker == "" {
datepicker = "gregorian"
}
return PageData{
Host: hostHeader,
BasePath: "/",
SId: subId,
Download: download,
Upload: upload,
Total: total,
Used: used,
Remained: remained,
Expire: expire,
LastOnline: lastOnline,
Datepicker: datepicker,
DownloadByte: downloadByte,
UploadByte: uploadByte,
TotalByte: totalByte,
SubUrl: subURL,
SubJsonUrl: subJsonURL,
Result: subs,
}
}
func getHostFromXFH(s string) (string, error) {
if strings.Contains(s, ":") {
realHost, _, err := net.SplitHostPort(s)
if err != nil {
return "", err
}
return realHost, nil
}
return s, nil
}
func parseInt64(s string) (int64, error) {
// handle potential quotes
s = strings.Trim(s, "\"'")
n, err := strconv.ParseInt(s, 10, 64)
return n, err
}

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
@import "../lib/style/index.less";
@import "../lib/style/components.less";
@green-6: #008771;
@primary-color: @green-6;
@border-radius-base: 1rem;
@progress-remaining-color: #EDEDED;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,6 +6,7 @@ class DBInbound {
this.up = 0; this.up = 0;
this.down = 0; this.down = 0;
this.total = 0; this.total = 0;
this.allTime = 0;
this.remark = ""; this.remark = "";
this.enable = true; this.enable = true;
this.expiryTime = 0; this.expiryTime = 0;
@@ -48,8 +49,8 @@ class DBInbound {
return this.protocol === Protocols.SHADOWSOCKS; return this.protocol === Protocols.SHADOWSOCKS;
} }
get isSocks() { get isMixed() {
return this.protocol === Protocols.SOCKS; return this.protocol === Protocols.MIXED;
} }
get isHTTP() { get isHTTP() {

View File

@@ -3,18 +3,16 @@ const Protocols = {
VLESS: 'vless', VLESS: 'vless',
TROJAN: 'trojan', TROJAN: 'trojan',
SHADOWSOCKS: 'shadowsocks', SHADOWSOCKS: 'shadowsocks',
DOKODEMO: 'dokodemo-door', TUNNEL: 'tunnel',
SOCKS: 'socks', MIXED: 'mixed',
HTTP: 'http', HTTP: 'http',
WIREGUARD: 'wireguard', WIREGUARD: 'wireguard',
}; };
const SSMethods = { const SSMethods = {
AES_256_GCM: 'aes-256-gcm', AES_256_GCM: 'aes-256-gcm',
AES_128_GCM: 'aes-128-gcm',
CHACHA20_POLY1305: 'chacha20-poly1305', CHACHA20_POLY1305: 'chacha20-poly1305',
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305', CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
XCHACHA20_POLY1305: 'xchacha20-poly1305',
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305', XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm', BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm', BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
@@ -559,7 +557,9 @@ class TlsStreamSettings extends XrayCommonClass {
disableSystemRoot = false, disableSystemRoot = false,
enableSessionResumption = false, enableSessionResumption = false,
certificates = [new TlsStreamSettings.Cert()], certificates = [new TlsStreamSettings.Cert()],
alpn = [ALPN_OPTION.H3, ALPN_OPTION.H2, ALPN_OPTION.HTTP1], alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1],
echServerKeys = '',
echForceQuery = 'none',
settings = new TlsStreamSettings.Settings() settings = new TlsStreamSettings.Settings()
) { ) {
super(); super();
@@ -573,6 +573,8 @@ class TlsStreamSettings extends XrayCommonClass {
this.enableSessionResumption = enableSessionResumption; this.enableSessionResumption = enableSessionResumption;
this.certs = certificates; this.certs = certificates;
this.alpn = alpn; this.alpn = alpn;
this.echServerKeys = echServerKeys;
this.echForceQuery = echForceQuery;
this.settings = settings; this.settings = settings;
} }
@@ -592,7 +594,7 @@ class TlsStreamSettings extends XrayCommonClass {
} }
if (!ObjectUtil.isEmpty(json.settings)) { if (!ObjectUtil.isEmpty(json.settings)) {
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure, json.settings.fingerprint, json.settings.serverName, json.settings.domains); settings = new TlsStreamSettings.Settings(json.settings.allowInsecure, json.settings.fingerprint, json.settings.echConfigList);
} }
return new TlsStreamSettings( return new TlsStreamSettings(
json.serverName, json.serverName,
@@ -605,6 +607,8 @@ class TlsStreamSettings extends XrayCommonClass {
json.enableSessionResumption, json.enableSessionResumption,
certs, certs,
json.alpn, json.alpn,
json.echServerKeys,
json.echForceQuery,
settings, settings,
); );
} }
@@ -621,6 +625,8 @@ class TlsStreamSettings extends XrayCommonClass {
enableSessionResumption: this.enableSessionResumption, enableSessionResumption: this.enableSessionResumption,
certificates: TlsStreamSettings.toJsonArray(this.certs), certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn, alpn: this.alpn,
echServerKeys: this.echServerKeys,
echForceQuery: this.echForceQuery,
settings: this.settings, settings: this.settings,
}; };
} }
@@ -633,7 +639,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
keyFile = '', keyFile = '',
certificate = '', certificate = '',
key = '', key = '',
ocspStapling = 3600,
oneTimeLoading = false, oneTimeLoading = false,
usage = USAGE_OPTION.ENCIPHERMENT, usage = USAGE_OPTION.ENCIPHERMENT,
buildChain = false, buildChain = false,
@@ -644,7 +649,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
this.keyFile = keyFile; this.keyFile = keyFile;
this.cert = Array.isArray(certificate) ? certificate.join('\n') : certificate; this.cert = Array.isArray(certificate) ? certificate.join('\n') : certificate;
this.key = Array.isArray(key) ? key.join('\n') : key; this.key = Array.isArray(key) ? key.join('\n') : key;
this.ocspStapling = ocspStapling;
this.oneTimeLoading = oneTimeLoading; this.oneTimeLoading = oneTimeLoading;
this.usage = usage; this.usage = usage;
this.buildChain = buildChain this.buildChain = buildChain
@@ -656,7 +660,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
true, true,
json.certificateFile, json.certificateFile,
json.keyFile, '', '', json.keyFile, '', '',
json.ocspStapling,
json.oneTimeLoading, json.oneTimeLoading,
json.usage, json.usage,
json.buildChain, json.buildChain,
@@ -666,7 +669,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
false, '', '', false, '', '',
json.certificate.join('\n'), json.certificate.join('\n'),
json.key.join('\n'), json.key.join('\n'),
json.ocspStapling,
json.oneTimeLoading, json.oneTimeLoading,
json.usage, json.usage,
json.buildChain, json.buildChain,
@@ -679,7 +681,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
return { return {
certificateFile: this.certFile, certificateFile: this.certFile,
keyFile: this.keyFile, keyFile: this.keyFile,
ocspStapling: this.ocspStapling,
oneTimeLoading: this.oneTimeLoading, oneTimeLoading: this.oneTimeLoading,
usage: this.usage, usage: this.usage,
buildChain: this.buildChain, buildChain: this.buildChain,
@@ -688,7 +689,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
return { return {
certificate: this.cert.split('\n'), certificate: this.cert.split('\n'),
key: this.key.split('\n'), key: this.key.split('\n'),
ocspStapling: this.ocspStapling,
oneTimeLoading: this.oneTimeLoading, oneTimeLoading: this.oneTimeLoading,
usage: this.usage, usage: this.usage,
buildChain: this.buildChain, buildChain: this.buildChain,
@@ -701,21 +701,25 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
constructor( constructor(
allowInsecure = false, allowInsecure = false,
fingerprint = UTLS_FINGERPRINT.UTLS_CHROME, fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
echConfigList = '',
) { ) {
super(); super();
this.allowInsecure = allowInsecure; this.allowInsecure = allowInsecure;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
this.echConfigList = echConfigList;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new TlsStreamSettings.Settings( return new TlsStreamSettings.Settings(
json.allowInsecure, json.allowInsecure,
json.fingerprint, json.fingerprint,
json.echConfigList,
); );
} }
toJson() { toJson() {
return { return {
allowInsecure: this.allowInsecure, allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint, fingerprint: this.fingerprint,
echConfigList: this.echConfigList
}; };
} }
}; };
@@ -725,25 +729,27 @@ class RealityStreamSettings extends XrayCommonClass {
constructor( constructor(
show = false, show = false,
xver = 0, xver = 0,
dest = 'yahoo.com:443', target = 'google.com:443',
serverNames = 'yahoo.com,www.yahoo.com', serverNames = 'google.com,www.google.com',
privateKey = '', privateKey = '',
minClient = '', minClientVer = '',
maxClient = '', maxClientVer = '',
maxTimediff = 0, maxTimediff = 0,
shortIds = RandomUtil.randomShortIds(), shortIds = RandomUtil.randomShortIds(),
mldsa65Seed = '',
settings = new RealityStreamSettings.Settings() settings = new RealityStreamSettings.Settings()
) { ) {
super(); super();
this.show = show; this.show = show;
this.xver = xver; this.xver = xver;
this.dest = dest; this.target = target;
this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames; this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames;
this.privateKey = privateKey; this.privateKey = privateKey;
this.minClient = minClient; this.minClientVer = minClientVer;
this.maxClient = maxClient; this.maxClientVer = maxClientVer;
this.maxTimediff = maxTimediff; this.maxTimediff = maxTimediff;
this.shortIds = Array.isArray(shortIds) ? shortIds.join(",") : shortIds; this.shortIds = Array.isArray(shortIds) ? shortIds.join(",") : shortIds;
this.mldsa65Seed = mldsa65Seed;
this.settings = settings; this.settings = settings;
} }
@@ -754,19 +760,21 @@ class RealityStreamSettings extends XrayCommonClass {
json.settings.publicKey, json.settings.publicKey,
json.settings.fingerprint, json.settings.fingerprint,
json.settings.serverName, json.settings.serverName,
json.settings.spiderX json.settings.spiderX,
json.settings.mldsa65Verify,
); );
} }
return new RealityStreamSettings( return new RealityStreamSettings(
json.show, json.show,
json.xver, json.xver,
json.dest, json.target,
json.serverNames, json.serverNames,
json.privateKey, json.privateKey,
json.minClient, json.minClientVer,
json.maxClient, json.maxClientVer,
json.maxTimediff, json.maxTimediff,
json.shortIds, json.shortIds,
json.mldsa65Seed,
settings, settings,
); );
} }
@@ -775,13 +783,14 @@ class RealityStreamSettings extends XrayCommonClass {
return { return {
show: this.show, show: this.show,
xver: this.xver, xver: this.xver,
dest: this.dest, target: this.target,
serverNames: this.serverNames.split(","), serverNames: this.serverNames.split(","),
privateKey: this.privateKey, privateKey: this.privateKey,
minClient: this.minClient, minClientVer: this.minClientVer,
maxClient: this.maxClient, maxClientVer: this.maxClientVer,
maxTimediff: this.maxTimediff, maxTimediff: this.maxTimediff,
shortIds: this.shortIds.split(","), shortIds: this.shortIds.split(","),
mldsa65Seed: this.mldsa65Seed,
settings: this.settings, settings: this.settings,
}; };
} }
@@ -792,13 +801,15 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
publicKey = '', publicKey = '',
fingerprint = UTLS_FINGERPRINT.UTLS_CHROME, fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
serverName = '', serverName = '',
spiderX = '/' spiderX = '/',
mldsa65Verify = ''
) { ) {
super(); super();
this.publicKey = publicKey; this.publicKey = publicKey;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
this.serverName = serverName; this.serverName = serverName;
this.spiderX = spiderX; this.spiderX = spiderX;
this.mldsa65Verify = mldsa65Verify;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new RealityStreamSettings.Settings( return new RealityStreamSettings.Settings(
@@ -806,6 +817,7 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
json.fingerprint, json.fingerprint,
json.serverName, json.serverName,
json.spiderX, json.spiderX,
json.mldsa65Verify
); );
} }
toJson() { toJson() {
@@ -814,6 +826,7 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
fingerprint: this.fingerprint, fingerprint: this.fingerprint,
serverName: this.serverName, serverName: this.serverName,
spiderX: this.spiderX, spiderX: this.spiderX,
mldsa65Verify: this.mldsa65Verify
}; };
} }
}; };
@@ -1027,27 +1040,6 @@ class Sniffing extends XrayCommonClass {
} }
} }
class Allocate extends XrayCommonClass {
constructor(
strategy = "always",
refresh = 5,
concurrency = 3,
) {
super();
this.strategy = strategy;
this.refresh = refresh;
this.concurrency = concurrency;
}
static fromJson(json = {}) {
return new Allocate(
json.strategy,
json.refresh,
json.concurrency,
);
}
}
class Inbound extends XrayCommonClass { class Inbound extends XrayCommonClass {
constructor( constructor(
port = RandomUtil.randomInteger(10000, 60000), port = RandomUtil.randomInteger(10000, 60000),
@@ -1057,7 +1049,6 @@ class Inbound extends XrayCommonClass {
streamSettings = new StreamSettings(), streamSettings = new StreamSettings(),
tag = '', tag = '',
sniffing = new Sniffing(), sniffing = new Sniffing(),
allocate = new Allocate(),
clientStats = '', clientStats = '',
) { ) {
super(); super();
@@ -1068,7 +1059,6 @@ class Inbound extends XrayCommonClass {
this.stream = streamSettings; this.stream = streamSettings;
this.tag = tag; this.tag = tag;
this.sniffing = sniffing; this.sniffing = sniffing;
this.allocate = allocate;
this.clientStats = clientStats; this.clientStats = clientStats;
} }
getClientStats() { getClientStats() {
@@ -1233,7 +1223,6 @@ class Inbound extends XrayCommonClass {
this.stream = new StreamSettings(); this.stream = new StreamSettings();
this.tag = ''; this.tag = '';
this.sniffing = new Sniffing(); this.sniffing = new Sniffing();
this.allocate = new Allocate();
} }
genVmessLink(address = '', port = this.port, forceTls, remark = '', clientId, security) { genVmessLink(address = '', port = this.port, forceTls, remark = '', clientId, security) {
@@ -1249,7 +1238,6 @@ class Inbound extends XrayCommonClass {
id: clientId, id: clientId,
scy: security, scy: security,
net: this.stream.network, net: this.stream.network,
type: 'none',
tls: tls, tls: tls,
}; };
const network = this.stream.network; const network = this.stream.network;
@@ -1284,7 +1272,7 @@ class Inbound extends XrayCommonClass {
const xhttp = this.stream.xhttp; const xhttp = this.stream.xhttp;
obj.path = xhttp.path; obj.path = xhttp.path;
obj.host = xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host'); obj.host = xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host');
obj.mode = xhttp.mode; obj.type = xhttp.mode;
} }
if (tls === 'tls') { if (tls === 'tls') {
@@ -1311,6 +1299,7 @@ class Inbound extends XrayCommonClass {
const security = forceTls == 'same' ? this.stream.security : forceTls; const security = forceTls == 'same' ? this.stream.security : forceTls;
const params = new Map(); const params = new Map();
params.set("type", this.stream.network); params.set("type", this.stream.network);
params.set("encryption", this.settings.encryption);
switch (type) { switch (type) {
case "tcp": case "tcp":
const tcp = this.stream.tcp; const tcp = this.stream.tcp;
@@ -1367,6 +1356,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", this.stream.tls.sni); params.set("sni", this.stream.tls.sni);
} }
if (this.stream.tls.settings.echConfigList?.length > 0) {
params.set("ech", this.stream.tls.settings.echConfigList);
}
if (type == "tcp" && !ObjectUtil.isEmpty(flow)) { if (type == "tcp" && !ObjectUtil.isEmpty(flow)) {
params.set("flow", flow); params.set("flow", flow);
} }
@@ -1386,6 +1378,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) { if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX); params.set("spx", this.stream.reality.settings.spiderX);
} }
if (!ObjectUtil.isEmpty(this.stream.reality.settings.mldsa65Verify)) {
params.set("pqv", this.stream.reality.settings.mldsa65Verify);
}
if (type == 'tcp' && !ObjectUtil.isEmpty(flow)) { if (type == 'tcp' && !ObjectUtil.isEmpty(flow)) {
params.set("flow", flow); params.set("flow", flow);
} }
@@ -1463,6 +1458,9 @@ class Inbound extends XrayCommonClass {
if (this.stream.tls.settings.allowInsecure) { if (this.stream.tls.settings.allowInsecure) {
params.set("allowInsecure", "1"); params.set("allowInsecure", "1");
} }
if (this.stream.tls.settings.echConfigList?.length > 0) {
params.set("ech", this.stream.tls.settings.echConfigList);
}
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", this.stream.tls.sni); params.set("sni", this.stream.tls.sni);
} }
@@ -1541,6 +1539,9 @@ class Inbound extends XrayCommonClass {
if (this.stream.tls.settings.allowInsecure) { if (this.stream.tls.settings.allowInsecure) {
params.set("allowInsecure", "1"); params.set("allowInsecure", "1");
} }
if (this.stream.tls.settings.echConfigList?.length > 0) {
params.set("ech", this.stream.tls.settings.echConfigList);
}
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", this.stream.tls.sni); params.set("sni", this.stream.tls.sni);
} }
@@ -1560,6 +1561,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) { if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX); params.set("spx", this.stream.reality.settings.spiderX);
} }
if (!ObjectUtil.isEmpty(this.stream.reality.settings.mldsa65Verify)) {
params.set("pqv", this.stream.reality.settings.mldsa65Verify);
}
} }
else { else {
@@ -1674,14 +1678,13 @@ class Inbound extends XrayCommonClass {
StreamSettings.fromJson(json.streamSettings), StreamSettings.fromJson(json.streamSettings),
json.tag, json.tag,
Sniffing.fromJson(json.sniffing), Sniffing.fromJson(json.sniffing),
Allocate.fromJson(json.allocate),
json.clientStats json.clientStats
) )
} }
toJson() { toJson() {
let streamSettings; let streamSettings;
if (this.canEnableStream()) { if (this.canEnableStream() || this.stream?.sockopt) {
streamSettings = this.stream.toJson(); streamSettings = this.stream.toJson();
} }
return { return {
@@ -1692,7 +1695,6 @@ class Inbound extends XrayCommonClass {
streamSettings: streamSettings, streamSettings: streamSettings,
tag: this.tag, tag: this.tag,
sniffing: this.sniffing.toJson(), sniffing: this.sniffing.toJson(),
allocate: this.allocate.toJson(),
clientStats: this.clientStats clientStats: this.clientStats
}; };
} }
@@ -1710,8 +1712,8 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.VLESS: return new Inbound.VLESSSettings(protocol); case Protocols.VLESS: return new Inbound.VLESSSettings(protocol);
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol); case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol); case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
case Protocols.DOKODEMO: return new Inbound.DokodemoSettings(protocol); case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol);
case Protocols.SOCKS: return new Inbound.SocksSettings(protocol); case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol); case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol); case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
default: return null; default: return null;
@@ -1724,8 +1726,8 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json); case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json);
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json); case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json); case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
case Protocols.DOKODEMO: return Inbound.DokodemoSettings.fromJson(json); case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json);
case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json); case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json); case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json); case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
default: return null; default: return null;
@@ -1788,7 +1790,9 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
tgId = '', tgId = '',
subId = RandomUtil.randomLowerAndNum(16), subId = RandomUtil.randomLowerAndNum(16),
comment = '', comment = '',
reset = 0 reset = 0,
created_at = undefined,
updated_at = undefined
) { ) {
super(); super();
this.id = id; this.id = id;
@@ -1802,6 +1806,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
this.subId = subId; this.subId = subId;
this.comment = comment; this.comment = comment;
this.reset = reset; this.reset = reset;
this.created_at = created_at;
this.updated_at = updated_at;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@@ -1817,6 +1823,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
json.subId, json.subId,
json.comment, json.comment,
json.reset, json.reset,
json.created_at,
json.updated_at,
); );
} }
get _expiryTime() { get _expiryTime() {
@@ -1850,13 +1858,17 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
constructor( constructor(
protocol, protocol,
vlesses = [new Inbound.VLESSSettings.VLESS()], vlesses = [new Inbound.VLESSSettings.VLESS()],
decryption = 'none', decryption = "none",
fallbacks = [] encryption = "none",
fallbacks = [],
selectedAuth = undefined,
) { ) {
super(protocol); super(protocol);
this.vlesses = vlesses; this.vlesses = vlesses;
this.decryption = decryption; this.decryption = decryption;
this.encryption = encryption;
this.fallbacks = fallbacks; this.fallbacks = fallbacks;
this.selectedAuth = selectedAuth;
} }
addFallback() { addFallback() {
@@ -1867,22 +1879,43 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
this.fallbacks.splice(index, 1); this.fallbacks.splice(index, 1);
} }
// decryption should be set to static value
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.VLESSSettings( const obj = new Inbound.VLESSSettings(
Protocols.VLESS, Protocols.VLESS,
json.clients.map(client => Inbound.VLESSSettings.VLESS.fromJson(client)), (json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
json.decryption || 'none', json.decryption,
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks),); json.encryption,
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
json.selectedAuth
);
return obj;
} }
toJson() { toJson() {
return { const json = {
clients: Inbound.VLESSSettings.toJsonArray(this.vlesses), clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
decryption: this.decryption,
fallbacks: Inbound.VLESSSettings.toJsonArray(this.fallbacks),
}; };
if (this.decryption) {
json.decryption = this.decryption;
}
if (this.encryption) {
json.encryption = this.encryption;
}
if (this.fallbacks && this.fallbacks.length > 0) {
json.fallbacks = Inbound.VLESSSettings.toJsonArray(this.fallbacks);
}
if (this.selectedAuth) {
json.selectedAuth = this.selectedAuth;
}
return json;
} }
}; };
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass { Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
@@ -1897,7 +1930,9 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
tgId = '', tgId = '',
subId = RandomUtil.randomLowerAndNum(16), subId = RandomUtil.randomLowerAndNum(16),
comment = '', comment = '',
reset = 0 reset = 0,
created_at = undefined,
updated_at = undefined
) { ) {
super(); super();
this.id = id; this.id = id;
@@ -1911,6 +1946,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
this.subId = subId; this.subId = subId;
this.comment = comment; this.comment = comment;
this.reset = reset; this.reset = reset;
this.created_at = created_at;
this.updated_at = updated_at;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@@ -1926,6 +1963,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
json.subId, json.subId,
json.comment, json.comment,
json.reset, json.reset,
json.created_at,
json.updated_at,
); );
} }
@@ -2036,7 +2075,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
tgId = '', tgId = '',
subId = RandomUtil.randomLowerAndNum(16), subId = RandomUtil.randomLowerAndNum(16),
comment = '', comment = '',
reset = 0 reset = 0,
created_at = undefined,
updated_at = undefined
) { ) {
super(); super();
this.password = password; this.password = password;
@@ -2049,6 +2090,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
this.subId = subId; this.subId = subId;
this.comment = comment; this.comment = comment;
this.reset = reset; this.reset = reset;
this.created_at = created_at;
this.updated_at = updated_at;
} }
toJson() { toJson() {
@@ -2063,6 +2106,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
subId: this.subId, subId: this.subId,
comment: this.comment, comment: this.comment,
reset: this.reset, reset: this.reset,
created_at: this.created_at,
updated_at: this.updated_at,
}; };
} }
@@ -2078,6 +2123,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
json.subId, json.subId,
json.comment, json.comment,
json.reset, json.reset,
json.created_at,
json.updated_at,
); );
} }
@@ -2150,7 +2197,7 @@ Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
Inbound.ShadowsocksSettings = class extends Inbound.Settings { Inbound.ShadowsocksSettings = class extends Inbound.Settings {
constructor(protocol, constructor(protocol,
method = SSMethods.BLAKE3_AES_256_GCM, method = SSMethods.BLAKE3_AES_256_GCM,
password = '', password = RandomUtil.randomShadowsocksPassword(),
network = 'tcp,udp', network = 'tcp,udp',
shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()], shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()],
ivCheck = false, ivCheck = false,
@@ -2188,7 +2235,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass { Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
constructor( constructor(
method = '', method = '',
password = '', password = RandomUtil.randomShadowsocksPassword(),
email = RandomUtil.randomLowerAndNum(8), email = RandomUtil.randomLowerAndNum(8),
limitIp = 0, limitIp = 0,
totalGB = 0, totalGB = 0,
@@ -2197,7 +2244,9 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
tgId = '', tgId = '',
subId = RandomUtil.randomLowerAndNum(16), subId = RandomUtil.randomLowerAndNum(16),
comment = '', comment = '',
reset = 0 reset = 0,
created_at = undefined,
updated_at = undefined
) { ) {
super(); super();
this.method = method; this.method = method;
@@ -2211,6 +2260,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
this.subId = subId; this.subId = subId;
this.comment = comment; this.comment = comment;
this.reset = reset; this.reset = reset;
this.created_at = created_at;
this.updated_at = updated_at;
} }
toJson() { toJson() {
@@ -2226,6 +2277,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
subId: this.subId, subId: this.subId,
comment: this.comment, comment: this.comment,
reset: this.reset, reset: this.reset,
created_at: this.created_at,
updated_at: this.updated_at,
}; };
} }
@@ -2242,6 +2295,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
json.subId, json.subId,
json.comment, json.comment,
json.reset, json.reset,
json.created_at,
json.updated_at,
); );
} }
@@ -2272,26 +2327,29 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
}; };
Inbound.DokodemoSettings = class extends Inbound.Settings { Inbound.TunnelSettings = class extends Inbound.Settings {
constructor( constructor(
protocol, protocol,
address, address,
port, port,
portMap = [],
network = 'tcp,udp', network = 'tcp,udp',
followRedirect = false followRedirect = false
) { ) {
super(protocol); super(protocol);
this.address = address; this.address = address;
this.port = port; this.port = port;
this.portMap = portMap;
this.network = network; this.network = network;
this.followRedirect = followRedirect; this.followRedirect = followRedirect;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.DokodemoSettings( return new Inbound.TunnelSettings(
Protocols.DOKODEMO, Protocols.TUNNEL,
json.address, json.address,
json.port, json.port,
XrayCommonClass.toHeaders(json.portMap),
json.network, json.network,
json.followRedirect, json.followRedirect,
); );
@@ -2301,14 +2359,15 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
return { return {
address: this.address, address: this.address,
port: this.port, port: this.port,
portMap: XrayCommonClass.toV2Headers(this.portMap, false),
network: this.network, network: this.network,
followRedirect: this.followRedirect, followRedirect: this.followRedirect,
}; };
} }
}; };
Inbound.SocksSettings = class extends Inbound.Settings { Inbound.MixedSettings = class extends Inbound.Settings {
constructor(protocol, auth = 'password', accounts = [new Inbound.SocksSettings.SocksAccount()], udp = false, ip = '127.0.0.1') { constructor(protocol, auth = 'password', accounts = [new Inbound.MixedSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
super(protocol); super(protocol);
this.auth = auth; this.auth = auth;
this.accounts = accounts; this.accounts = accounts;
@@ -2328,11 +2387,11 @@ Inbound.SocksSettings = class extends Inbound.Settings {
let accounts; let accounts;
if (json.auth === 'password') { if (json.auth === 'password') {
accounts = json.accounts.map( accounts = json.accounts.map(
account => Inbound.SocksSettings.SocksAccount.fromJson(account) account => Inbound.MixedSettings.SocksAccount.fromJson(account)
) )
} }
return new Inbound.SocksSettings( return new Inbound.MixedSettings(
Protocols.SOCKS, Protocols.MIXED,
json.auth, json.auth,
accounts, accounts,
json.udp, json.udp,
@@ -2349,7 +2408,7 @@ Inbound.SocksSettings = class extends Inbound.Settings {
}; };
} }
}; };
Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass { Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) { constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
super(); super();
this.user = user; this.user = user;
@@ -2357,7 +2416,7 @@ Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass {
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.SocksSettings.SocksAccount(json.user, json.pass); return new Inbound.MixedSettings.SocksAccount(json.user, json.pass);
} }
}; };

View File

@@ -354,13 +354,15 @@ class TlsStreamSettings extends CommonClass {
serverName = '', serverName = '',
alpn = [], alpn = [],
fingerprint = '', fingerprint = '',
allowInsecure = false allowInsecure = false,
echConfigList = '',
) { ) {
super(); super();
this.serverName = serverName; this.serverName = serverName;
this.alpn = alpn; this.alpn = alpn;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
this.allowInsecure = allowInsecure; this.allowInsecure = allowInsecure;
this.echConfigList = echConfigList;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@@ -369,6 +371,7 @@ class TlsStreamSettings extends CommonClass {
json.alpn, json.alpn,
json.fingerprint, json.fingerprint,
json.allowInsecure, json.allowInsecure,
json.echConfigList,
); );
} }
@@ -378,6 +381,7 @@ class TlsStreamSettings extends CommonClass {
alpn: this.alpn, alpn: this.alpn,
fingerprint: this.fingerprint, fingerprint: this.fingerprint,
allowInsecure: this.allowInsecure, allowInsecure: this.allowInsecure,
echConfigList: this.echConfigList
}; };
} }
} }
@@ -388,7 +392,8 @@ class RealityStreamSettings extends CommonClass {
fingerprint = '', fingerprint = '',
serverName = '', serverName = '',
shortId = '', shortId = '',
spiderX = '/' spiderX = '',
mldsa65Verify = ''
) { ) {
super(); super();
this.publicKey = publicKey; this.publicKey = publicKey;
@@ -396,6 +401,7 @@ class RealityStreamSettings extends CommonClass {
this.serverName = serverName; this.serverName = serverName;
this.shortId = shortId this.shortId = shortId
this.spiderX = spiderX; this.spiderX = spiderX;
this.mldsa65Verify = mldsa65Verify;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new RealityStreamSettings( return new RealityStreamSettings(
@@ -404,6 +410,7 @@ class RealityStreamSettings extends CommonClass {
json.serverName, json.serverName,
json.shortId, json.shortId,
json.spiderX, json.spiderX,
json.mldsa65Verify
); );
} }
toJson() { toJson() {
@@ -413,6 +420,7 @@ class RealityStreamSettings extends CommonClass {
serverName: this.serverName, serverName: this.serverName,
shortId: this.shortId, shortId: this.shortId,
spiderX: this.spiderX, spiderX: this.spiderX,
mldsa65Verify: this.mldsa65Verify
}; };
} }
}; };
@@ -616,11 +624,27 @@ class Outbound extends CommonClass {
} }
canEnableMux() { canEnableMux() {
if (this.settings.flow && this.settings.flow != '') { // Disable Mux if flow is set
if (this.settings.flow && this.settings.flow !== '') {
this.mux.enabled = false; this.mux.enabled = false;
return false; return false;
} }
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.HTTP, Protocols.Socks].includes(this.protocol);
// Disable Mux if network is xhttp
if (this.stream.network === 'xhttp') {
this.mux.enabled = false;
return false;
}
// Allow Mux only for these protocols
return [
Protocols.VMess,
Protocols.VLESS,
Protocols.Trojan,
Protocols.Shadowsocks,
Protocols.HTTP,
Protocols.Socks
].includes(this.protocol);
} }
hasVnext() { hasVnext() {
@@ -762,7 +786,8 @@ class Outbound extends CommonClass {
let alpn = url.searchParams.get('alpn'); let alpn = url.searchParams.get('alpn');
let allowInsecure = url.searchParams.get('allowInsecure'); let allowInsecure = url.searchParams.get('allowInsecure');
let sni = url.searchParams.get('sni') ?? ''; let sni = url.searchParams.get('sni') ?? '';
stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, allowInsecure == 1); let ech = url.searchParams.get('ech') ?? '';
stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, allowInsecure == 1, ech);
} }
if (security == 'reality') { if (security == 'reality') {
@@ -771,7 +796,8 @@ class Outbound extends CommonClass {
let sni = url.searchParams.get('sni') ?? ''; let sni = url.searchParams.get('sni') ?? '';
let sid = url.searchParams.get('sid') ?? ''; let sid = url.searchParams.get('sid') ?? '';
let spx = url.searchParams.get('spx') ?? ''; let spx = url.searchParams.get('spx') ?? '';
stream.reality = new RealityStreamSettings(pbk, fp, sni, sid, spx); let pqv = url.searchParams.get('pqv') ?? '';
stream.reality = new RealityStreamSettings(pbk, fp, sni, sid, spx, pqv);
} }
const regex = /([^@]+):\/\/([^@]+)@(.+):(\d+)(.*)$/; const regex = /([^@]+):\/\/([^@]+)@(.+):(\d+)(.*)$/;
@@ -787,7 +813,7 @@ class Outbound extends CommonClass {
var settings; var settings;
switch (protocol) { switch (protocol) {
case Protocols.VLESS: case Protocols.VLESS:
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? ''); settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '', url.searchParams.get('encryption') ?? 'none');
break; break;
case Protocols.Trojan: case Protocols.Trojan:
settings = new Outbound.TrojanSettings(address, port, userData); settings = new Outbound.TrojanSettings(address, port, userData);
@@ -893,12 +919,14 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
constructor( constructor(
packets = '1-3', packets = '1-3',
length = '', length = '',
interval = '' interval = '',
maxSplit = ''
) { ) {
super(); super();
this.packets = packets; this.packets = packets;
this.length = length; this.length = length;
this.interval = interval; this.interval = interval;
this.maxSplit = maxSplit;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@@ -906,6 +934,7 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
json.packets, json.packets,
json.length, json.length,
json.interval, json.interval,
json.maxSplit
); );
} }
}; };
@@ -914,12 +943,14 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
constructor( constructor(
type = 'rand', type = 'rand',
packet = '10-20', packet = '10-20',
delay = '10-16' delay = '10-16',
applyTo = 'ip'
) { ) {
super(); super();
this.type = type; this.type = type;
this.packet = packet; this.packet = packet;
this.delay = delay; this.delay = delay;
this.applyTo = applyTo;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@@ -927,6 +958,7 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
json.type, json.type,
json.packet, json.packet,
json.delay, json.delay,
json.applyTo
); );
} }
@@ -935,6 +967,7 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
type: this.type, type: this.type,
packet: this.packet, packet: this.packet,
delay: this.delay, delay: this.delay,
applyTo: this.applyTo
}; };
} }
}; };
@@ -962,7 +995,7 @@ Outbound.DNSSettings = class extends CommonClass {
network = 'udp', network = 'udp',
address = '', address = '',
port = 53, port = 53,
nonIPQuery = 'drop', nonIPQuery = 'reject',
blockTypes = [] blockTypes = []
) { ) {
super(); super();
@@ -1013,13 +1046,13 @@ Outbound.VmessSettings = class extends CommonClass {
} }
}; };
Outbound.VLESSSettings = class extends CommonClass { Outbound.VLESSSettings = class extends CommonClass {
constructor(address, port, id, flow, encryption = 'none') { constructor(address, port, id, flow, encryption) {
super(); super();
this.address = address; this.address = address;
this.port = port; this.port = port;
this.id = id; this.id = id;
this.flow = flow; this.flow = flow;
this.encryption = encryption this.encryption = encryption;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@@ -1038,7 +1071,7 @@ Outbound.VLESSSettings = class extends CommonClass {
vnext: [{ vnext: [{
address: this.address, address: this.address,
port: this.port, port: this.port,
users: [{ id: this.id, flow: this.flow, encryption: 'none', }], users: [{ id: this.id, flow: this.flow, encryption: this.encryption }],
}], }],
}; };
} }

View File

@@ -7,7 +7,7 @@ class AllSetting {
this.webCertFile = ""; this.webCertFile = "";
this.webKeyFile = ""; this.webKeyFile = "";
this.webBasePath = "/"; this.webBasePath = "/";
this.sessionMaxAge = 60; this.sessionMaxAge = 360;
this.pageSize = 50; this.pageSize = 50;
this.expireDiff = 0; this.expireDiff = 0;
this.trafficDiff = 0; this.trafficDiff = 0;

View File

@@ -0,0 +1,125 @@
(function () {
// Vue app for Subscription page
const el = document.getElementById('subscription-data');
if (!el) return;
const textarea = document.getElementById('subscription-links');
const rawLinks = (textarea?.value || '').split('\n').filter(Boolean);
const data = {
sId: el.getAttribute('data-sid') || '',
subUrl: el.getAttribute('data-sub-url') || '',
subJsonUrl: el.getAttribute('data-subjson-url') || '',
download: el.getAttribute('data-download') || '',
upload: el.getAttribute('data-upload') || '',
used: el.getAttribute('data-used') || '',
total: el.getAttribute('data-total') || '',
remained: el.getAttribute('data-remained') || '',
expireMs: (parseInt(el.getAttribute('data-expire') || '0', 10) || 0) * 1000,
lastOnlineMs: (parseInt(el.getAttribute('data-lastonline') || '0', 10) || 0),
downloadByte: parseInt(el.getAttribute('data-downloadbyte') || '0', 10) || 0,
uploadByte: parseInt(el.getAttribute('data-uploadbyte') || '0', 10) || 0,
totalByte: parseInt(el.getAttribute('data-totalbyte') || '0', 10) || 0,
datepicker: el.getAttribute('data-datepicker') || 'gregorian',
};
// Normalize lastOnline to milliseconds if it looks like seconds
if (data.lastOnlineMs && data.lastOnlineMs < 10_000_000_000) {
data.lastOnlineMs *= 1000;
}
function renderLink(item) {
return (
Vue.h('a-list-item', {}, [
Vue.h('a-space', { props: { size: 'small' } }, [
Vue.h('a-button', { props: { size: 'small' }, on: { click: () => copy(item) } }, [Vue.h('a-icon', { props: { type: 'copy' } })]),
Vue.h('span', { class: 'break-all' }, item)
])
])
);
}
function copy(text) {
ClipboardManager.copyText(text).then(ok => {
const messageType = ok ? 'success' : 'error';
Vue.prototype.$message[messageType](ok ? 'Copied' : 'Copy failed');
});
}
function open(url) {
window.location.href = url;
}
function drawQR(value) {
try { new QRious({ element: document.getElementById('qrcode'), value, size: 220 }); } catch (e) { console.warn(e); }
}
// Try to extract a human label (email/ps) from different link types
function linkName(link, idx) {
try {
if (link.startsWith('vmess://')) {
const json = JSON.parse(atob(link.replace('vmess://', '')));
if (json.ps) return json.ps;
if (json.add && json.id) return json.add; // fallback host
} else if (link.startsWith('vless://') || link.startsWith('trojan://')) {
// vless://<id>@host:port?...#name
const hashIdx = link.indexOf('#');
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
// email sometimes in query params like sni or remark
const qIdx = link.indexOf('?');
if (qIdx !== -1) {
const qs = new URL('http://x/?' + link.substring(qIdx + 1, hashIdx !== -1 ? hashIdx : undefined)).searchParams;
if (qs.get('remark')) return qs.get('remark');
if (qs.get('email')) return qs.get('email');
}
// else take user@host
const at = link.indexOf('@');
const protSep = link.indexOf('://');
if (at !== -1 && protSep !== -1) return link.substring(protSep + 3, at);
} else if (link.startsWith('ss://')) {
// shadowsocks: label often after #
const hashIdx = link.indexOf('#');
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
}
} catch (e) { /* ignore and fallback */ }
return 'Link ' + (idx + 1);
}
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
themeSwitcher,
app: data,
links: rawLinks,
lang: '',
viewportWidth: (typeof window !== 'undefined' ? window.innerWidth : 1024),
},
async mounted() {
this.lang = LanguageManager.getLanguage();
// Discover subJsonUrl if provided via template bootstrap
const tpl = document.getElementById('subscription-data');
const sj = tpl ? tpl.getAttribute('data-subjson-url') : '';
if (sj) this.app.subJsonUrl = sj;
drawQR(this.app.subUrl);
// Draw second QR if available
try { new QRious({ element: document.getElementById('qrcode-subjson'), value: this.app.subJsonUrl || '', size: 220 }); } catch (e) { /* ignore */ }
// Track viewport width for responsive behavior
this._onResize = () => { this.viewportWidth = window.innerWidth; };
window.addEventListener('resize', this._onResize);
},
beforeDestroy() {
if (this._onResize) window.removeEventListener('resize', this._onResize);
},
computed: {
isMobile() { return this.viewportWidth < 576; },
isUnlimited() { return !this.app.totalByte; },
isActive() {
const now = Date.now();
const expiryOk = !this.app.expireMs || this.app.expireMs >= now;
const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte;
return expiryOk && trafficOk;
},
},
methods: { renderLink, copy, open, linkName, i18nLabel(key) { return '{{ i18n "' + key + '" }}'; } },
});
})();

View File

@@ -134,7 +134,7 @@ class DateUtil {
} }
static formatMillis(millis) { static formatMillis(millis) {
return moment(millis).format('YYYY-M-D H:m:s'); return moment(millis).format('YYYY-M-D HH:mm:ss');
} }
static firstDayOfMonth() { static firstDayOfMonth() {

View File

@@ -326,6 +326,14 @@ class ObjectUtil {
return false; return false;
} }
} }
for (const key in b) {
if (!b.hasOwnProperty(key)) {
continue;
}
if (!a.hasOwnProperty(key)) {
return false;
}
}
return true; return true;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
if (process.env.NODE_ENV === 'production') {
module.exports = require('./vue.common.prod.js')
} else {
module.exports = require('./vue.common.dev.js')
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
if (process.env.NODE_ENV === 'production') {
module.exports = require('./vue.runtime.common.prod.js')
} else {
module.exports = require('./vue.runtime.common.dev.js')
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,76 +0,0 @@
import Vue from './vue.runtime.common.js'
export default Vue
// this should be kept in sync with src/v3/index.ts
export const {
version,
// refs
ref,
shallowRef,
isRef,
toRef,
toRefs,
unref,
proxyRefs,
customRef,
triggerRef,
computed,
// reactive
reactive,
isReactive,
isReadonly,
isShallow,
isProxy,
shallowReactive,
markRaw,
toRaw,
readonly,
shallowReadonly,
// watch
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
// effectScope
effectScope,
onScopeDispose,
getCurrentScope,
// provide / inject
provide,
inject,
// lifecycle
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured,
onActivated,
onDeactivated,
onServerPrefetch,
onRenderTracked,
onRenderTriggered,
// v2 only
set,
del,
// v3 compat
h,
getCurrentInstance,
useSlots,
useAttrs,
mergeDefaults,
nextTick,
useCssModule,
useCssVars,
defineComponent,
defineAsyncComponent
} = Vue

View File

@@ -9,6 +9,7 @@ import (
type APIController struct { type APIController struct {
BaseController BaseController
inboundController *InboundController inboundController *InboundController
serverController *ServerController
Tgbot service.Tgbot Tgbot service.Tgbot
} }
@@ -19,41 +20,22 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
} }
func (a *APIController) initRouter(g *gin.RouterGroup) { func (a *APIController) initRouter(g *gin.RouterGroup) {
g = g.Group("/panel/api/inbounds") // Main API group
g.Use(a.checkLogin) api := g.Group("/panel/api")
api.Use(a.checkLogin)
a.inboundController = NewInboundController(g) // Inbounds API
inbounds := api.Group("/inbounds")
a.inboundController = NewInboundController(inbounds)
inboundRoutes := []struct { // Server API
Method string server := api.Group("/server")
Path string a.serverController = NewServerController(server)
Handler gin.HandlerFunc
}{
{"GET", "/createbackup", a.createBackup},
{"GET", "/list", a.inboundController.getInbounds},
{"GET", "/get/:id", a.inboundController.getInbound},
{"GET", "/getClientTraffics/:email", a.inboundController.getClientTraffics},
{"GET", "/getClientTrafficsById/:id", a.inboundController.getClientTrafficsById},
{"POST", "/add", a.inboundController.addInbound},
{"POST", "/del/:id", a.inboundController.delInbound},
{"POST", "/update/:id", a.inboundController.updateInbound},
{"POST", "/clientIps/:email", a.inboundController.getClientIps},
{"POST", "/clearClientIps/:email", a.inboundController.clearClientIps},
{"POST", "/addClient", a.inboundController.addInboundClient},
{"POST", "/:id/delClient/:clientId", a.inboundController.delInboundClient},
{"POST", "/updateClient/:clientId", a.inboundController.updateInboundClient},
{"POST", "/:id/resetClientTraffic/:email", a.inboundController.resetClientTraffic},
{"POST", "/resetAllTraffics", a.inboundController.resetAllTraffics},
{"POST", "/resetAllClientTraffics/:id", a.inboundController.resetAllClientTraffics},
{"POST", "/delDepletedClients/:id", a.inboundController.delDepletedClients},
{"POST", "/onlines", a.inboundController.onlines},
}
for _, route := range inboundRoutes { // Extra routes
g.Handle(route.Method, route.Path, route.Handler) api.GET("/backuptotgbot", a.BackuptoTgbot)
}
} }
func (a *APIController) createBackup(c *gin.Context) { func (a *APIController) BackuptoTgbot(c *gin.Context) {
a.Tgbot.SendBackupToAdmins() a.Tgbot.SendBackupToAdmins()
} }

View File

@@ -24,9 +24,12 @@ func NewInboundController(g *gin.RouterGroup) *InboundController {
} }
func (a *InboundController) initRouter(g *gin.RouterGroup) { func (a *InboundController) initRouter(g *gin.RouterGroup) {
g = g.Group("/inbound")
g.POST("/list", a.getInbounds) g.GET("/list", a.getInbounds)
g.GET("/get/:id", a.getInbound)
g.GET("/getClientTraffics/:email", a.getClientTraffics)
g.GET("/getClientTrafficsById/:id", a.getClientTrafficsById)
g.POST("/add", a.addInbound) g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound) g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound) g.POST("/update/:id", a.updateInbound)
@@ -41,6 +44,9 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/delDepletedClients/:id", a.delDepletedClients) g.POST("/delDepletedClients/:id", a.delDepletedClients)
g.POST("/import", a.importInbound) g.POST("/import", a.importInbound)
g.POST("/onlines", a.onlines) g.POST("/onlines", a.onlines)
g.POST("/lastOnline", a.lastOnline)
g.POST("/updateClientTraffic/:email", a.updateClientTraffic)
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
} }
func (a *InboundController) getInbounds(c *gin.Context) { func (a *InboundController) getInbounds(c *gin.Context) {
@@ -108,8 +114,8 @@ func (a *InboundController) addInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
} }
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, err) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, nil)
if err == nil && needRestart { if needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@@ -126,8 +132,8 @@ func (a *InboundController) delInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
} }
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, err) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, nil)
if err == nil && needRestart { if needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@@ -152,8 +158,8 @@ func (a *InboundController) updateInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
} }
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, err) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, nil)
if err == nil && needRestart { if needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@@ -339,3 +345,53 @@ func (a *InboundController) delDepletedClients(c *gin.Context) {
func (a *InboundController) onlines(c *gin.Context) { func (a *InboundController) onlines(c *gin.Context) {
jsonObj(c, a.inboundService.GetOnlineClients(), nil) jsonObj(c, a.inboundService.GetOnlineClients(), nil)
} }
func (a *InboundController) lastOnline(c *gin.Context) {
data, err := a.inboundService.GetClientsLastOnline()
jsonObj(c, data, err)
}
func (a *InboundController) updateClientTraffic(c *gin.Context) {
email := c.Param("email")
// Define the request structure for traffic update
type TrafficUpdateRequest struct {
Upload int64 `json:"upload"`
Download int64 `json:"download"`
}
var request TrafficUpdateRequest
err := c.ShouldBindJSON(&request)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
return
}
err = a.inboundService.UpdateClientTrafficByEmail(email, request.Upload, request.Download)
if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
}
func (a *InboundController) delInboundClientByEmail(c *gin.Context) {
inboundId, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, "Invalid inbound ID", err)
return
}
email := c.Param("email")
needRestart, err := a.inboundService.DelInboundClientByEmail(inboundId, email)
if err != nil {
jsonMsg(c, "Failed to delete client by email", err)
return
}
jsonMsg(c, "Client deleted successfully", nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}

View File

@@ -17,7 +17,8 @@ var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
type ServerController struct { type ServerController struct {
BaseController BaseController
serverService service.ServerService serverService service.ServerService
settingService service.SettingService
lastStatus *service.Status lastStatus *service.Status
lastGetStatusTime time.Time lastGetStatusTime time.Time
@@ -36,20 +37,26 @@ func NewServerController(g *gin.RouterGroup) *ServerController {
} }
func (a *ServerController) initRouter(g *gin.RouterGroup) { func (a *ServerController) initRouter(g *gin.RouterGroup) {
g = g.Group("/server")
g.Use(a.checkLogin) g.GET("/status", a.status)
g.POST("/status", a.status) g.GET("/getXrayVersion", a.getXrayVersion)
g.POST("/getXrayVersion", a.getXrayVersion) g.GET("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb)
g.GET("/getNewUUID", a.getNewUUID)
g.GET("/getNewX25519Cert", a.getNewX25519Cert)
g.GET("/getNewmldsa65", a.getNewmldsa65)
g.GET("/getNewmlkem768", a.getNewmlkem768)
g.GET("/getNewVlessEnc", a.getNewVlessEnc)
g.POST("/stopXrayService", a.stopXrayService) g.POST("/stopXrayService", a.stopXrayService)
g.POST("/restartXrayService", a.restartXrayService) g.POST("/restartXrayService", a.restartXrayService)
g.POST("/installXray/:version", a.installXray) g.POST("/installXray/:version", a.installXray)
g.POST("/updateGeofile", a.updateGeofile)
g.POST("/updateGeofile/:fileName", a.updateGeofile) g.POST("/updateGeofile/:fileName", a.updateGeofile)
g.POST("/logs/:count", a.getLogs) g.POST("/logs/:count", a.getLogs)
g.POST("/getConfigJson", a.getConfigJson) g.POST("/xraylogs/:count", a.getXrayLogs)
g.GET("/getDb", a.getDb)
g.POST("/importDB", a.importDB) g.POST("/importDB", a.importDB)
g.POST("/getNewX25519Cert", a.getNewX25519Cert) g.POST("/getNewEchCert", a.getNewEchCert)
} }
func (a *ServerController) refreshStatus() { func (a *ServerController) refreshStatus() {
@@ -132,6 +139,50 @@ func (a *ServerController) getLogs(c *gin.Context) {
jsonObj(c, logs, nil) jsonObj(c, logs, nil)
} }
func (a *ServerController) getXrayLogs(c *gin.Context) {
count := c.Param("count")
filter := c.PostForm("filter")
showDirect := c.PostForm("showDirect")
showBlocked := c.PostForm("showBlocked")
showProxy := c.PostForm("showProxy")
var freedoms []string
var blackholes []string
//getting tags for freedom and blackhole outbounds
config, err := a.settingService.GetDefaultXrayConfig()
if err == nil && config != nil {
if cfgMap, ok := config.(map[string]interface{}); ok {
if outbounds, ok := cfgMap["outbounds"].([]interface{}); ok {
for _, outbound := range outbounds {
if obMap, ok := outbound.(map[string]interface{}); ok {
switch obMap["protocol"] {
case "freedom":
if tag, ok := obMap["tag"].(string); ok {
freedoms = append(freedoms, tag)
}
case "blackhole":
if tag, ok := obMap["tag"].(string); ok {
blackholes = append(blackholes, tag)
}
}
}
}
}
}
}
if len(freedoms) == 0 {
freedoms = []string{"direct"}
}
if len(blackholes) == 0 {
blackholes = []string{"blocked"}
}
logs := a.serverService.GetXrayLogs(count, filter, showDirect, showBlocked, showProxy, freedoms, blackholes)
jsonObj(c, logs, nil)
}
func (a *ServerController) getConfigJson(c *gin.Context) { func (a *ServerController) getConfigJson(c *gin.Context) {
configJson, err := a.serverService.GetConfigJson() configJson, err := a.serverService.GetConfigJson()
if err != nil { if err != nil {
@@ -198,3 +249,50 @@ func (a *ServerController) getNewX25519Cert(c *gin.Context) {
} }
jsonObj(c, cert, nil) jsonObj(c, cert, nil)
} }
func (a *ServerController) getNewmldsa65(c *gin.Context) {
cert, err := a.serverService.GetNewmldsa65()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.getNewmldsa65Error"), err)
return
}
jsonObj(c, cert, nil)
}
func (a *ServerController) getNewEchCert(c *gin.Context) {
sni := c.PostForm("sni")
cert, err := a.serverService.GetNewEchCert(sni)
if err != nil {
jsonMsg(c, "get ech certificate", err)
return
}
jsonObj(c, cert, nil)
}
func (a *ServerController) getNewVlessEnc(c *gin.Context) {
out, err := a.serverService.GetNewVlessEnc()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.getNewVlessEncError"), err)
return
}
jsonObj(c, out, nil)
}
func (a *ServerController) getNewUUID(c *gin.Context) {
uuidResp, err := a.serverService.GetNewUUID()
if err != nil {
jsonMsg(c, "Failed to generate UUID", err)
return
}
jsonObj(c, uuidResp, nil)
}
func (a *ServerController) getNewmlkem768(c *gin.Context) {
out, err := a.serverService.GetNewmlkem768()
if err != nil {
jsonMsg(c, "Failed to generate mlkem768 keys", err)
return
}
jsonObj(c, out, nil)
}

View File

@@ -8,6 +8,7 @@ type XUIController struct {
BaseController BaseController
inboundController *InboundController inboundController *InboundController
serverController *ServerController
settingController *SettingController settingController *SettingController
xraySettingController *XraySettingController xraySettingController *XraySettingController
} }
@@ -28,6 +29,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
g.GET("/xray", a.xraySettings) g.GET("/xray", a.xraySettings)
a.inboundController = NewInboundController(g) a.inboundController = NewInboundController(g)
a.serverController = NewServerController(g)
a.settingController = NewSettingController(g) a.settingController = NewSettingController(g)
a.xraySettingController = NewXraySettingController(g) a.xraySettingController = NewXraySettingController(g)
} }

View File

@@ -2,10 +2,10 @@ package entity
import ( import (
"crypto/tls" "crypto/tls"
"math"
"net" "net"
"strings" "strings"
"time" "time"
"math"
"x-ui/util/common" "x-ui/util/common"
) )
@@ -39,8 +39,8 @@ type AllSetting struct {
TgCpu int `json:"tgCpu" form:"tgCpu"` TgCpu int `json:"tgCpu" form:"tgCpu"`
TgLang string `json:"tgLang" form:"tgLang"` TgLang string `json:"tgLang" form:"tgLang"`
TimeLocation string `json:"timeLocation" form:"timeLocation"` TimeLocation string `json:"timeLocation" form:"timeLocation"`
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"` TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"` TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
SubEnable bool `json:"subEnable" form:"subEnable"` SubEnable bool `json:"subEnable" form:"subEnable"`
SubTitle string `json:"subTitle" form:"subTitle"` SubTitle string `json:"subTitle" form:"subTitle"`
SubListen string `json:"subListen" form:"subListen"` SubListen string `json:"subListen" form:"subListen"`

View File

@@ -33,12 +33,17 @@
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch> <a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
</template> </template>
<template slot="online" slot-scope="text, client, index"> <template slot="online" slot-scope="text, client, index">
<template v-if="client.enable && isClientOnline(client.email)"> <a-popover :overlay-class-name="themeSwitcher.currentTheme">
<a-tag color="green">{{ i18n "online" }}</a-tag> <template slot="content" >
</template> {{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
<template v-else> </template>
<a-tag>{{ i18n "offline" }}</a-tag> <template v-if="client.enable && isClientOnline(client.email)">
</template> <a-tag color="green">{{ i18n "online" }}</a-tag>
</template>
<template v-else>
<a-tag>{{ i18n "offline" }}</a-tag>
</template>
</a-popover>
</template> </template>
<template slot="client" slot-scope="text, client"> <template slot="client" slot-scope="text, client">
<a-space direction="horizontal" :size="2"> <a-space direction="horizontal" :size="2">
@@ -98,6 +103,10 @@
</table> </table>
</a-popover> </a-popover>
</template> </template>
<template slot="allTime" slot-scope="text, client">
<a-tag>[[ SizeFormatter.sizeFormat(getAllTimeClient(record, client.email)) ]]</a-tag>
</template>
<template slot="expiryTime" slot-scope="text, client, index"> <template slot="expiryTime" slot-scope="text, client, index">
<template v-if="client.expiryTime !=0 && client.reset >0"> <template v-if="client.expiryTime !=0 && client.reset >0">
<a-popover :overlay-class-name="themeSwitcher.currentTheme"> <a-popover :overlay-class-name="themeSwitcher.currentTheme">
@@ -278,4 +287,30 @@
</a-badge> </a-badge>
</a-popover> </a-popover>
</template> </template>
<template slot="createdAt" slot-scope="text, client, index">
<template v-if="client.created_at">
<template v-if="app.datepicker === 'gregorian'">
[[ DateUtil.formatMillis(client.created_at) ]]
</template>
<template v-else>
[[ DateUtil.convertToJalalian(moment(client.created_at)) ]]
</template>
</template>
<template v-else>
-
</template>
</template>
<template slot="updatedAt" slot-scope="text, client, index">
<template v-if="client.updated_at">
<template v-if="app.datepicker === 'gregorian'">
[[ DateUtil.formatMillis(client.updated_at) ]]
</template>
<template v-else>
[[ DateUtil.convertToJalalian(moment(client.updated_at)) ]]
</template>
</template>
<template v-else>
-
</template>
</template>
{{end}} {{end}}

View File

@@ -1,15 +0,0 @@
{{define "form/allocate"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='Strategy'>
<a-select v-model="inbound.allocate.strategy" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['always','random']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Refresh'>
<a-input-number v-model.number="inbound.allocate.refresh" min="0"></a-input-number>
</a-form-item>
<a-form-item label='Concurrency'>
<a-input-number v-model.number="inbound.allocate.concurrency" min="0"></a-input-number>
</a-form-item>
</a-form>
{{end}}

View File

@@ -44,7 +44,7 @@
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable"> <a-form-item v-if="client.email && app.subSettings?.enable">
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">

View File

@@ -83,14 +83,14 @@
{{template "form/shadowsocks"}} {{template "form/shadowsocks"}}
</template> </template>
<!-- dokodemo-door --> <!-- tunnel -->
<template v-if="inbound.protocol === Protocols.DOKODEMO"> <template v-if="inbound.protocol === Protocols.TUNNEL">
{{template "form/dokodemo"}} {{template "form/tunnel"}}
</template> </template>
<!-- socks --> <!-- mixed -->
<template v-if="inbound.protocol === Protocols.SOCKS"> <template v-if="inbound.protocol === Protocols.MIXED">
{{template "form/socks"}} {{template "form/mixed"}}
</template> </template>
<!-- http --> <!-- http -->
@@ -121,13 +121,4 @@
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<!-- allocate -->
<!-- Temporarily disabled until we accepts range for port allocation
<a-collapse>
<a-collapse-panel header='Allocate'>
{{template "form/allocate"}}
</a-collapse-panel>
</a-collapse>
-->
{{end}} {{end}}

View File

@@ -42,6 +42,9 @@
<a-form-item label='Interval'> <a-form-item label='Interval'>
<a-input v-model.trim="outbound.settings.fragment.interval"></a-input> <a-input v-model.trim="outbound.settings.fragment.interval"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Max Split'>
<a-input v-model.trim="outbound.settings.fragment.maxSplit"></a-input>
</a-form-item>
</template> </template>
<!-- Switch for Noises --> <!-- Switch for Noises -->
@@ -75,6 +78,11 @@
<a-form-item label='Delay'> <a-form-item label='Delay'>
<a-input v-model.trim="noise.delay"></a-input> <a-input v-model.trim="noise.delay"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Apply To'>
<a-select v-model="noise.applyTo" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['ip','ipv4','ipv6']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</a-form> </a-form>
</template> </template>
</template> </template>
@@ -97,7 +105,7 @@
</a-form-item> </a-form-item>
<a-form-item label='non-IP queries'> <a-form-item label='non-IP queries'>
<a-select v-model="outbound.settings.nonIPQuery" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.nonIPQuery" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['drop','skip']" :value="s">[[ s ]]</a-select-option> <a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[ s ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="outbound.settings.nonIPQuery === 'skip'" label='Block Types' > <a-form-item v-if="outbound.settings.nonIPQuery === 'skip'" label='Block Types' >
@@ -218,6 +226,11 @@
</template> </template>
<!-- vless settings --> <!-- vless settings -->
<template v-if="outbound.protocol === Protocols.VLESS">
<a-form-item label='encryption'>
<a-input v-model.trim="outbound.settings.encryption"></a-input>
</a-form-item>
</template>
<template v-if="outbound.canEnableTlsFlow()"> <template v-if="outbound.canEnableTlsFlow()">
<a-form-item label='Flow'> <a-form-item label='Flow'>
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
@@ -428,6 +441,9 @@
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option> <a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="ECH Config List">
<a-input v-model.trim="outbound.stream.tls.echConfigList"></a-input>
</a-form-item>
<a-form-item label="Allow Insecure"> <a-form-item label="Allow Insecure">
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch> <a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
</a-form-item> </a-form-item>
@@ -444,13 +460,16 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="Short ID"> <a-form-item label="Short ID">
<a-input v-model.trim="outbound.stream.reality.shortId" :style="{ width: '250px' }"></a-input> <a-input v-model.trim="outbound.stream.reality.shortId"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="SpiderX"> <a-form-item label="SpiderX">
<a-input v-model.trim="outbound.stream.reality.spiderX" :style="{ width: '250px' }"></a-input> <a-input v-model.trim="outbound.stream.reality.spiderX"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Public Key"> <a-form-item label="Public Key">
<a-input v-model.trim="outbound.stream.reality.publicKey"></a-input> <a-textarea v-model.trim="outbound.stream.reality.publicKey"></a-textarea>
</a-form-item>
<a-form-item label="mldsa65 Verify">
<a-textarea v-model.trim="outbound.stream.reality.mldsa65Verify"></a-textarea>
</a-form-item> </a-form-item>
</template> </template>
</template> </template>

View File

@@ -1,4 +1,4 @@
{{define "form/dokodemo"}} {{define "form/tunnel"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'> <a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
<a-input v-model.trim="inbound.settings.address"></a-input> <a-input v-model.trim="inbound.settings.address"></a-input>
@@ -6,6 +6,19 @@
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'> <a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
<a-input-number v-model.number="inbound.settings.port"></a-input-number> <a-input-number v-model.number="inbound.settings.port"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.portMap"}}'>
<a-button size="small" @click="inbound.settings.portMap.push({name: '', value: ''})">+</a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(pm, index) in inbound.settings.portMap">
<a-input style="width: 50%" v-model.trim="pm.name" placeholder='{{ i18n "pages.inbounds.port"}}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="pm.value" placeholder='{{ i18n "pages.inbounds.targetAddress" }}'>
<a-button slot="addonAfter" size="small" @click="inbound.settings.portMap.splice(index,1)">-</a-button>
</a-input>
</a-input-group>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network"}}'> <a-form-item label='{{ i18n "pages.inbounds.network"}}'>
<a-select v-model="inbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="inbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp,udp">TCP,UDP</a-select-option> <a-select-option value="tcp,udp">TCP,UDP</a-select-option>
@@ -17,4 +30,8 @@
<a-switch v-model="inbound.settings.followRedirect"></a-switch> <a-switch v-model="inbound.settings.followRedirect"></a-switch>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- sockopt -->
<template>
{{template "form/streamSockopt"}}
</template>
{{end}} {{end}}

View File

@@ -1,4 +1,4 @@
{{define "form/socks"}} {{define "form/mixed"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'> <a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
<a-switch v-model="inbound.settings.udp"></a-switch> <a-switch v-model="inbound.settings.udp"></a-switch>
@@ -15,7 +15,7 @@
<td width="45%">{{ i18n "username" }}</td> <td width="45%">{{ i18n "username" }}</td>
<td width="45%">{{ i18n "password" }}</td> <td width="45%">{{ i18n "password" }}</td>
<td> <td>
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())"></a-button> <a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"></a-button>
</td> </td>
</tr> </tr>
</table> </table>

View File

@@ -18,7 +18,29 @@
</table> </table>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<template v-if="inbound.isTcp"> <template v-if="!inbound.stream.isTLS || !inbound.stream.isReality">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Authentication">
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="X25519, not Post-Quantum">X25519 (not Post-Quantum)</a-select-option>
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768 (Post-Quantum)</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="decryption">
<a-input v-model.trim="inbound.settings.decryption"></a-input>
</a-form-item>
<a-form-item label="encryption">
<a-input v-model="inbound.settings.encryption"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-space>
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New keys</a-button>
<a-button danger @click="clearVlessEnc">Clear</a-button>
</a-space>
</a-form-item>
</a-form>
</template>
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button> <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>

View File

@@ -12,8 +12,8 @@
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Dest (Target)'> <a-form-item label='Target'>
<a-input v-model.trim="inbound.stream.reality.dest"></a-input> <a-input v-model.trim="inbound.stream.reality.target"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='SNI'> <a-form-item label='SNI'>
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input> <a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
@@ -21,14 +21,12 @@
<a-form-item label='Max Time Diff (ms)'> <a-form-item label='Max Time Diff (ms)'>
<a-input-number v-model.number="inbound.stream.reality.maxTimediff" :min="0"></a-input-number> <a-input-number v-model.number="inbound.stream.reality.maxTimediff" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<!-- we also have this but i think it's not necessary <a-form-item label='Min Client Ver'>
<a-form-item label='Min Client'> <a-input v-model.trim="inbound.stream.reality.minClientVer"></a-input>
<a-input v-model.trim="inbound.stream.reality.minClient"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Max Client'> <a-form-item label='Max Client Ver'>
<a-input v-model.trim="inbound.stream.reality.maxClient"></a-input> <a-input v-model.trim="inbound.stream.reality.maxClientVer"></a-input>
</a-form-item> </a-form-item>
-->
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
@@ -38,19 +36,34 @@
type="sync"></a-icon> type="sync"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input> <a-textarea v-model.trim="inbound.stream.reality.shortIds"></a-textarea>
</a-form-item> </a-form-item>
<a-form-item label='SpiderX'> <a-form-item label='SpiderX'>
<a-input v-model.trim="inbound.stream.reality.settings.spiderX"></a-input> <a-input v-model.trim="inbound.stream.reality.settings.spiderX"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'> <a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input v-model="inbound.stream.reality.settings.publicKey"></a-input> <a-textarea v-model="inbound.stream.reality.settings.publicKey"></a-textarea>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'> <a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input type="password" v-model="inbound.stream.reality.privateKey"></a-input> <a-textarea v-model="inbound.stream.reality.privateKey"></a-textarea>
</a-form-item> </a-form-item>
<a-form-item label=" "> <a-form-item label=" ">
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button> <a-space>
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
<a-button danger @click="clearX25519Cert">Clear</a-button>
</a-space>
</a-form-item>
<a-form-item label="mldsa65 Seed">
<a-textarea v-model="inbound.stream.reality.mldsa65Seed"></a-textarea>
</a-form-item>
<a-form-item label="mldsa65 Verify">
<a-textarea v-model="inbound.stream.reality.settings.mldsa65Verify"></a-textarea>
</a-form-item>
<a-form-item label=" ">
<a-space>
<a-button type="primary" icon="import" @click="getNewmldsa65">Get New Seed</a-button>
<a-button danger @click="clearMldsa65">Clear</a-button>
</a-space>
</a-form-item> </a-form-item>
</template> </template>
{{end}} {{end}}

View File

@@ -85,15 +85,12 @@
</template> </template>
<template v-else> <template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'> <a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input v-model="cert.cert"></a-input> <a-textarea v-model="cert.cert"></a-textarea>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'> <a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input type="password" v-model="cert.key"></a-input> <a-textarea v-model="cert.key"></a-textarea>
</a-form-item> </a-form-item>
</template> </template>
<a-form-item label='OCSP stapling'>
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="One Time Loading"> <a-form-item label="One Time Loading">
<a-switch v-model="cert.oneTimeLoading"></a-switch> <a-switch v-model="cert.oneTimeLoading"></a-switch>
</a-form-item> </a-form-item>
@@ -106,6 +103,24 @@
<a-switch v-model="cert.buildChain"></a-switch> <a-switch v-model="cert.buildChain"></a-switch>
</a-form-item> </a-form-item>
</template> </template>
<a-form-item label='ECH key'>
<a-input v-model="inbound.stream.tls.echServerKeys"></a-input>
</a-form-item>
<a-form-item label='ECH config'>
<a-input v-model="inbound.stream.tls.settings.echConfigList"></a-input>
</a-form-item>
<a-form-item label='ECH force query'>
<a-select v-model="inbound.stream.tls.echForceQuery"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in ['none', 'half', 'full']" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label=" ">
<a-space>
<a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button>
<a-button danger @click="clearEchCert">Clear</a-button>
</a-space>
</a-form-item>
</template> </template>
<!-- reality settings --> <!-- reality settings -->

View File

@@ -1,150 +1,8 @@
{{ template "page/head_start" .}} {{ template "page/head_start" .}}
<style>
.ant-table:not(.ant-table-expanded-row .ant-table) {
outline: 1px solid #f0f0f0;
outline-offset: -1px;
border-radius: 1rem;
overflow-x: hidden;
}
.dark .ant-table:not(.ant-table-expanded-row .ant-table) {
outline-color: var(--dark-color-table-ring);
}
.ant-table .ant-table-content .ant-table-scroll .ant-table-body {
overflow-y: hidden;
}
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
margin:-10px 22px !important;
}
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper .ant-table {
border-bottom-left-radius: 1rem;
border-bottom-right-radius: 1rem;
}
.ant-table .ant-table-content .ant-table-tbody tr:last-child tr:last-child td {
border-bottom-color: transparent;
}
.ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:first-child {
border-bottom-left-radius: 6px;
}
.ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:last-child {
border-bottom-right-radius: 6px;
}
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
@media (max-width: 768px) {
.ant-card-body {
padding: .5rem;
}
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
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;
}
tr.hideExpandIcon .ant-table-row-expand-icon {
display: none;
}
.infinite-tag {
padding: 0 5px;
border-radius: 2rem;
min-width: 50px;
min-height: 22px;
}
.infinite-bar .ant-progress-inner .ant-progress-bg {
background-color: #F2EAF1;
border: #D5BED2 solid 1px;
}
.dark .infinite-bar .ant-progress-inner .ant-progress-bg {
background-color: #7a316f !important;
border: #7a316f solid 1px;
}
.ant-collapse {
margin: 5px 0;
}
.info-large-tag {
max-width: 200px;
overflow: hidden;
}
.client-comment {
font-size: 12px;
opacity: 0.75;
cursor: help;
}
.client-email {
font-weight: 500;
}
.client-popup-item {
display: flex;
align-items: center;
gap: 5px;
}
.online-animation .ant-badge-status-dot {
animation: onlineAnimation 1.2s linear infinite;
}
@keyframes onlineAnimation {
0%,
50%,
100% {
transform: scale(1);
opacity: 1;
}
10% {
transform: scale(1.5);
opacity: .2;
}
}
.tr-table-box {
display: flex;
gap: 4px;
justify-content: center;
align-items: center;
}
.tr-table-rt {
flex-basis: 70px;
min-width: 70px;
text-align: end;
}
.tr-table-lt {
flex-basis: 70px;
min-width: 70px;
text-align: start;
}
.tr-table-bar {
flex-basis: 160px;
min-width: 60px;
}
.tr-infinity-ch {
font-size: 14pt;
max-height: 24px;
display: inline-flex;
align-items: center;
}
.ant-table-expanded-row .ant-table .ant-table-body {
overflow-x: hidden;
}
.ant-table-expanded-row .ant-table-tbody>tr>td {
padding: 10px 2px;
}
.ant-table-expanded-row .ant-table-thead>tr>th {
padding: 12px 2px;
}
</style>
{{ template "page/head_end" .}} {{ template "page/head_end" .}}
{{ template "page/body_start" .}} {{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme"> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' inbounds-page'">
<a-sidebar></a-sidebar> <a-sidebar></a-sidebar>
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content> <a-layout-content>
@@ -167,28 +25,35 @@
<a-col> <a-col>
<a-card size="small" :style="{ padding: '16px' }" hoverable> <a-card size="small" :style="{ padding: '16px' }" hoverable>
<a-row> <a-row>
<a-col :sm="12" :md="6"> <a-col :sm="12" :md="5">
<a-custom-statistic title='{{ i18n "pages.inbounds.totalDownUp" }}' :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`"> <a-custom-statistic title='{{ i18n "pages.inbounds.totalDownUp" }}' :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`">
<template #prefix> <template #prefix>
<a-icon type="swap"></a-icon> <a-icon type="swap"></a-icon>
</template> </template>
</a-custom-statistic> </a-custom-statistic>
</a-col> </a-col>
<a-col :sm="12" :md="6"> <a-col :sm="12" :md="5">
<a-custom-statistic title='{{ i18n "pages.inbounds.totalUsage" }}' :value="SizeFormatter.sizeFormat(total.up + total.down)" :style="{ marginTop: isMobile ? '10px' : 0 }"> <a-custom-statistic title='{{ i18n "pages.inbounds.totalUsage" }}' :value="SizeFormatter.sizeFormat(total.up + total.down)" :style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix> <template #prefix>
<a-icon type="pie-chart"></a-icon> <a-icon type="pie-chart"></a-icon>
</template> </template>
</a-custom-statistic> </a-custom-statistic>
</a-col> </a-col>
<a-col :sm="12" :md="6"> <a-col :sm="12" :md="5">
<a-custom-statistic title='{{ i18n "pages.inbounds.allTimeTrafficUsage" }}' :value="SizeFormatter.sizeFormat(total.allTime)" :style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix>
<a-icon type="history"></a-icon>
</template>
</a-custom-statistic>
</a-col>
<a-col :sm="12" :md="5">
<a-custom-statistic title='{{ i18n "pages.inbounds.inboundCount" }}' :value="dbInbounds.length" :style="{ marginTop: isMobile ? '10px' : 0 }"> <a-custom-statistic title='{{ i18n "pages.inbounds.inboundCount" }}' :value="dbInbounds.length" :style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix> <template #prefix>
<a-icon type="bars"></a-icon> <a-icon type="bars"></a-icon>
</template> </template>
</a-custom-statistic> </a-custom-statistic>
</a-col> </a-col>
<a-col :sm="12" :md="6"> <a-col :sm="12" :md="4">
<a-custom-statistic title='{{ i18n "clients" }}' value=" " :style="{ marginTop: isMobile ? '10px' : 0 }"> <a-custom-statistic title='{{ i18n "clients" }}' value=" " :style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix> <template #prefix>
<a-space direction="horizontal"> <a-space direction="horizontal">
@@ -405,9 +270,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@@ -419,9 +284,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@@ -433,9 +298,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@@ -447,9 +312,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@@ -484,6 +349,9 @@
</a-tag> </a-tag>
</a-popover> </a-popover>
</template> </template>
<template slot="allTimeInbound" slot-scope="text, dbInbound">
<a-tag>[[ SizeFormatter.sizeFormat(dbInbound.allTime || 0) ]]</a-tag>
</template>
<template slot="enable" slot-scope="text, dbInbound"> <template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch> <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
</template> </template>
@@ -534,9 +402,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@@ -548,9 +416,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@@ -562,9 +430,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@@ -576,9 +444,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@@ -696,7 +564,7 @@
}, { }, {
title: '{{ i18n "pages.inbounds.enable" }}', title: '{{ i18n "pages.inbounds.enable" }}',
align: 'center', align: 'center',
width: 30, width: 35,
scopedSlots: { customRender: 'enable' }, scopedSlots: { customRender: 'enable' },
}, { }, {
title: '{{ i18n "pages.inbounds.remark" }}', title: '{{ i18n "pages.inbounds.remark" }}',
@@ -721,8 +589,13 @@
}, { }, {
title: '{{ i18n "pages.inbounds.traffic" }}', title: '{{ i18n "pages.inbounds.traffic" }}',
align: 'center', align: 'center',
width: 60, width: 90,
scopedSlots: { customRender: 'traffic' }, scopedSlots: { customRender: 'traffic' },
}, {
title: '{{ i18n "pages.inbounds.allTimeTraffic" }}',
align: 'center',
width: 70,
scopedSlots: { customRender: 'allTimeInbound' },
}, { }, {
title: '{{ i18n "pages.inbounds.expireDate" }}', title: '{{ i18n "pages.inbounds.expireDate" }}',
align: 'center', align: 'center',
@@ -755,10 +628,11 @@
const innerColumns = [ const innerColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 65, scopedSlots: { customRender: 'actions' } }, { title: '{{ i18n "pages.inbounds.operate" }}', width: 65, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } }, { title: '{{ i18n "pages.inbounds.enable" }}', width: 35, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "online" }}', width: 30, scopedSlots: { customRender: 'online' } }, { title: '{{ i18n "online" }}', width: 32, scopedSlots: { customRender: 'online' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
]; ];
@@ -791,6 +665,7 @@
defaultKey: '', defaultKey: '',
clientCount: [], clientCount: [],
onlineClients: [], onlineClients: [],
lastOnlineMap: {},
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false, isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshing: false, refreshing: false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000, refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
@@ -811,25 +686,15 @@
loading(spinning = true) { loading(spinning = true) {
this.loadingStates.spinning = spinning; this.loadingStates.spinning = spinning;
}, },
getClientWithComment(email, inboundId) {
const dbInbound = this.dbInbounds.find(inbound => inbound.id === inboundId);
if (!dbInbound) return { email, comment: '' };
const inboundSettings = JSON.parse(dbInbound.settings);
if (inboundSettings.clients) {
const client = inboundSettings.clients.find(c => c.email === email);
return client ? { email: client.email, comment: client.comment || '' } : { email, comment: '' };
}
return { email, comment: '' };
},
async getDBInbounds() { async getDBInbounds() {
this.refreshing = true; this.refreshing = true;
const msg = await HttpUtil.post('/panel/inbound/list'); const msg = await HttpUtil.get('/panel/api/inbounds/list');
if (!msg.success) { if (!msg.success) {
this.refreshing = false; this.refreshing = false;
return; return;
} }
await this.getLastOnlineMap();
await this.getOnlineUsers(); await this.getOnlineUsers();
this.setInbounds(msg.obj); this.setInbounds(msg.obj);
@@ -838,12 +703,17 @@
}, 500); }, 500);
}, },
async getOnlineUsers() { async getOnlineUsers() {
const msg = await HttpUtil.post('/panel/inbound/onlines'); const msg = await HttpUtil.post('/panel/api/inbounds/onlines');
if (!msg.success) { if (!msg.success) {
return; return;
} }
this.onlineClients = msg.obj != null ? msg.obj : []; this.onlineClients = msg.obj != null ? msg.obj : [];
}, },
async getLastOnlineMap() {
const msg = await HttpUtil.post('/panel/api/inbounds/lastOnline');
if (!msg.success || !msg.obj) return;
this.lastOnlineMap = msg.obj || {}
},
async getDefaultSettings() { async getDefaultSettings() {
const msg = await HttpUtil.post('/panel/setting/defaultSettings'); const msg = await HttpUtil.post('/panel/setting/defaultSettings');
if (!msg.success) { if (!msg.success) {
@@ -893,7 +763,7 @@
} }
}, },
getClientCounts(dbInbound, inbound) { getClientCounts(dbInbound, inbound) {
let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = []; let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = [], comments = new Map();
clients = inbound.clients; clients = inbound.clients;
clientStats = dbInbound.clientStats clientStats = dbInbound.clientStats
now = new Date().getTime() now = new Date().getTime()
@@ -901,6 +771,9 @@
clientCount = clients.length; clientCount = clients.length;
if (dbInbound.enable) { if (dbInbound.enable) {
clients.forEach(client => { clients.forEach(client => {
if (client.comment) {
comments.set(client.email, client.comment)
}
if (client.enable) { if (client.enable) {
active.push(client.email); active.push(client.email);
if (this.isClientOnline(client.email)) online.push(client.email); if (this.isClientOnline(client.email)) online.push(client.email);
@@ -929,6 +802,7 @@
depleted: depleted, depleted: depleted,
expiring: expiring, expiring: expiring,
online: online, online: online,
comments: comments,
}; };
}, },
@@ -967,11 +841,13 @@
const list = this.clientCount[inbound.id][this.filterBy]; const list = this.clientCount[inbound.id][this.filterBy];
if (list.length > 0) { if (list.length > 0) {
const filteredSettings = { "clients": [] }; const filteredSettings = { "clients": [] };
inboundSettings.clients.forEach(client => { if (inboundSettings.clients) {
if (list.includes(client.email)) { inboundSettings.clients.forEach(client => {
filteredSettings.clients.push(client); if (list.includes(client.email)) {
} filteredSettings.clients.push(client);
}); }
});
}
newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings); newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
this.searchedInbounds.push(newInbound); this.searchedInbounds.push(newInbound);
} }
@@ -1082,9 +958,8 @@
settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(), settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
streamSettings: baseInbound.stream.toString(), streamSettings: baseInbound.stream.toString(),
sniffing: baseInbound.sniffing.toString(), sniffing: baseInbound.sniffing.toString(),
allocate: baseInbound.allocate.toString(),
}; };
await this.submit('/panel/inbound/add', data, inModal); await this.submit('/panel/api/inbounds/add', data, inModal);
}, },
openAddInbound() { openAddInbound() {
inModal.show({ inModal.show({
@@ -1126,11 +1001,14 @@
protocol: inbound.protocol, protocol: inbound.protocol,
settings: inbound.settings.toString(), settings: inbound.settings.toString(),
}; };
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString(); if (inbound.canEnableStream()){
data.streamSettings = inbound.stream.toString();
} else if (inbound.stream?.sockopt) {
data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);
}
data.sniffing = inbound.sniffing.toString(); data.sniffing = inbound.sniffing.toString();
data.allocate = inbound.allocate.toString();
await this.submit('/panel/inbound/add', data, inModal); await this.submit('/panel/api/inbounds/add', data, inModal);
}, },
async updateInbound(inbound, dbInbound) { async updateInbound(inbound, dbInbound) {
const data = { const data = {
@@ -1146,11 +1024,14 @@
protocol: inbound.protocol, protocol: inbound.protocol,
settings: inbound.settings.toString(), settings: inbound.settings.toString(),
}; };
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString(); if (inbound.canEnableStream()){
data.streamSettings = inbound.stream.toString();
} else if (inbound.stream?.sockopt) {
data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);
}
data.sniffing = inbound.sniffing.toString(); data.sniffing = inbound.sniffing.toString();
data.allocate = inbound.allocate.toString();
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal); await this.submit(`/panel/api/inbounds/update/${dbInbound.id}`, data, inModal);
}, },
openAddClient(dbInboundId) { openAddClient(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -1205,14 +1086,14 @@
id: dbInboundId, id: dbInboundId,
settings: '{"clients": [' + clients.toString() + ']}', settings: '{"clients": [' + clients.toString() + ']}',
}; };
await this.submit(`/panel/inbound/addClient`, data, modal); await this.submit(`/panel/api/inbounds/addClient`, data, modal);
}, },
async updateClient(client, dbInboundId, clientId) { async updateClient(client, dbInboundId, clientId) {
const data = { const data = {
id: dbInboundId, id: dbInboundId,
settings: '{"clients": [' + client.toString() + ']}', settings: '{"clients": [' + client.toString() + ']}',
}; };
await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal); await this.submit(`/panel/api/inbounds/updateClient/${clientId}`, data, clientModal);
}, },
resetTraffic(dbInboundId) { resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -1237,7 +1118,7 @@
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/del/' + dbInboundId), onOk: () => this.submit('/panel/api/inbounds/del/' + dbInboundId),
}); });
}, },
delClient(dbInboundId, client,confirmation = true) { delClient(dbInboundId, client,confirmation = true) {
@@ -1250,10 +1131,10 @@
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`), onOk: () => this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`),
}); });
} else { } else {
this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`); this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`);
} }
}, },
getSubGroupClients(dbInbounds, currentClient) { getSubGroupClients(dbInbounds, currentClient) {
@@ -1332,7 +1213,7 @@
switchEnable(dbInboundId,state) { switchEnable(dbInboundId,state) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
dbInbound.enable = state; dbInbound.enable = state;
this.submit(`/panel/inbound/update/${dbInboundId}`, dbInbound); this.submit(`/panel/api/inbounds/update/${dbInboundId}`, dbInbound);
}, },
async switchEnableClient(dbInboundId, client) { async switchEnableClient(dbInboundId, client) {
this.loading() this.loading()
@@ -1362,10 +1243,10 @@
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email), onOk: () => this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email),
}) })
} else { } else {
this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email); this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email);
} }
}, },
resetAllTraffic() { resetAllTraffic() {
@@ -1375,7 +1256,7 @@
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/resetAllTraffics'), onOk: () => this.submit('/panel/api/inbounds/resetAllTraffics'),
}); });
}, },
resetAllClientTraffics(dbInboundId) { resetAllClientTraffics(dbInboundId) {
@@ -1385,7 +1266,7 @@
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/resetAllClientTraffics/' + dbInboundId), onOk: () => this.submit('/panel/api/inbounds/resetAllClientTraffics/' + dbInboundId),
}) })
}, },
delDepletedClients(dbInboundId) { delDepletedClients(dbInboundId) {
@@ -1395,7 +1276,7 @@
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/delDepletedClients/' + dbInboundId), onOk: () => this.submit('/panel/api/inbounds/delDepletedClients/' + dbInboundId),
}) })
}, },
isExpiry(dbInbound, index) { isExpiry(dbInbound, index) {
@@ -1416,6 +1297,12 @@
clientStats = dbInbound.clientStats.find(stats => stats.email === email); clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return clientStats ? clientStats.up + clientStats.down : 0; return clientStats ? clientStats.up + clientStats.down : 0;
}, },
getAllTimeClient(dbInbound, email) {
if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
if (!clientStats) return 0;
return clientStats.allTime || (clientStats.up + clientStats.down);
},
getRemStats(dbInbound, email) { getRemStats(dbInbound, email) {
if (email.length == 0) return 0; if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email); clientStats = dbInbound.clientStats.find(stats => stats.email === email);
@@ -1473,6 +1360,17 @@
isClientOnline(email) { isClientOnline(email) {
return this.onlineClients.includes(email); return this.onlineClients.includes(email);
}, },
getLastOnline(email) {
return this.lastOnlineMap[email] || null
},
formatLastOnline(email) {
const ts = this.getLastOnline(email)
if (!ts) return '-'
if (this.datepicker === 'gregorian') {
return DateUtil.formatMillis(ts)
}
return DateUtil.convertToJalalian(moment(ts))
},
isRemovable(dbInboundId) { isRemovable(dbInboundId) {
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1; return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
}, },
@@ -1504,7 +1402,7 @@
value: '', value: '',
okText: '{{ i18n "pages.inbounds.import" }}', okText: '{{ i18n "pages.inbounds.import" }}',
confirm: async (dbInboundText) => { confirm: async (dbInboundText) => {
await this.submit('/panel/inbound/import', {data: dbInboundText}, promptModal); await this.submit('/panel/api/inbounds/import', {data: dbInboundText}, promptModal);
}, },
}); });
}, },
@@ -1605,11 +1503,12 @@
}, },
computed: { computed: {
total() { total() {
let down = 0, up = 0; let down = 0, up = 0, allTime = 0;
let clients = 0, deactive = [], depleted = [], expiring = []; let clients = 0, deactive = [], depleted = [], expiring = [];
this.dbInbounds.forEach(dbInbound => { this.dbInbounds.forEach(dbInbound => {
down += dbInbound.down; down += dbInbound.down;
up += dbInbound.up; up += dbInbound.up;
allTime += (dbInbound.allTime || (dbInbound.up + dbInbound.down));
if (this.clientCount[dbInbound.id]) { if (this.clientCount[dbInbound.id]) {
clients += this.clientCount[dbInbound.id].clients; clients += this.clientCount[dbInbound.id].clients;
deactive = deactive.concat(this.clientCount[dbInbound.id].deactive); deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
@@ -1620,6 +1519,7 @@
return { return {
down: down, down: down,
up: up, up: up,
allTime: allTime,
clients: clients, clients: clients,
deactive: deactive, deactive: deactive,
depleted: depleted, depleted: depleted,

View File

@@ -1,73 +1,4 @@
{{ template "page/head_start" .}} {{ template "page/head_start" .}}
<style>
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
.ant-card-dark h2 {
color: var(--dark-color-text-primary);
}
.ant-backup-list-item {
gap: 10px;
}
.ant-version-list-item {
--padding: 12px;
padding: var(--padding) !important;
gap: var(--padding);
}
.dark .ant-version-list-item svg{
color: var(--dark-color-text-primary);
}
.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-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>
{{ template "page/head_end" .}} {{ template "page/head_end" .}}
{{ template "page/body_start" .}} {{ template "page/body_start" .}}
@@ -77,7 +8,7 @@
<a-layout-content> <a-layout-content>
<a-spin :spinning="loadingStates.spinning" :delay="200" :tip="loadingTip"> <a-spin :spinning="loadingStates.spinning" :delay="200" :tip="loadingTip">
<transition name="list" appear> <transition name="list" appear>
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }" <a-alert type="error" v-if="showAlert && loadingStates.fetched" class="mb-10"
message='{{ i18n "secAlertTitle" }}' message='{{ i18n "secAlertTitle" }}'
color="red" color="red"
description='{{ i18n "secAlertSsl" }}' description='{{ i18n "secAlertSsl" }}'
@@ -87,7 +18,7 @@
<transition name="list" appear> <transition name="list" appear>
<template> <template>
<a-row v-if="!loadingStates.fetched"> <a-row v-if="!loadingStates.fetched">
<a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }"> <a-card class="card-placeholder text-center">
<a-spin tip='{{ i18n "loading" }}'></a-spin> <a-spin tip='{{ i18n "loading" }}'></a-spin>
</a-card> </a-card>
</a-row> </a-row>
@@ -97,7 +28,7 @@
<a-row :gutter="[0, isMobile ? 16 : 0]"> <a-row :gutter="[0, isMobile ? 16 : 0]">
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-row> <a-row>
<a-col :span="12" :style="{ textAlign: 'center' }"> <a-col :span="12" class="text-center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color" :stroke-color="status.cpu.color"
:percent="status.cpu.percent"></a-progress> :percent="status.cpu.percent"></a-progress>
@@ -112,7 +43,7 @@
</a-tooltip> </a-tooltip>
</div> </div>
</a-col> </a-col>
<a-col :span="12" :style="{ textAlign: 'center' }"> <a-col :span="12" class="text-center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color" :stroke-color="status.mem.color"
:percent="status.mem.percent"></a-progress> :percent="status.mem.percent"></a-progress>
@@ -124,7 +55,7 @@
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-row> <a-row>
<a-col :span="12" :style="{ textAlign: 'center' }"> <a-col :span="12" class="text-center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color" :stroke-color="status.swap.color"
:percent="status.swap.percent"></a-progress> :percent="status.swap.percent"></a-progress>
@@ -132,7 +63,7 @@
<b>{{ i18n "pages.index.swap" }}:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] / [[ SizeFormatter.sizeFormat(status.swap.total) ]] <b>{{ i18n "pages.index.swap" }}:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] / [[ SizeFormatter.sizeFormat(status.swap.total) ]]
</div> </div>
</a-col> </a-col>
<a-col :span="12" :style="{ textAlign: 'center' }"> <a-col :span="12" class="text-center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color" :stroke-color="status.disk.color"
:percent="status.disk.percent"></a-progress> :percent="status.disk.percent"></a-progress>
@@ -167,27 +98,31 @@
<span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span> <span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
</a-col> </a-col>
<a-col> <a-col>
<a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-tag> <a-icon type="bars" class="cursor-pointer float-right" @click="openLogs()"></a-icon>
</a-col> </a-col>
</a-row> </a-row>
</span> </span>
<template slot="content"> <template slot="content">
<span :style="{ maxWidth: '400px' }" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span> <span class="max-w-400" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span>
</template> </template>
<a-badge :text="status.xray.stateMsg" :color="status.xray.color"/> <a-badge :text="status.xray.stateMsg" :color="status.xray.color"/>
</a-popover> </a-popover>
</template> </template>
</template> </template>
<template #actions> <template #actions>
<a-space direction="horizontal" @click="stopXrayService" :style="{ justifyContent: 'center' }"> <a-space v-if="app.ipLimitEnable" direction="horizontal" @click="openXrayLogs()" class="jc-center">
<a-icon type="bars"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
</a-space>
<a-space direction="horizontal" @click="stopXrayService" class="jc-center">
<a-icon type="poweroff"></a-icon> <a-icon type="poweroff"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span> <span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
</a-space> </a-space>
<a-space direction="horizontal" @click="restartXrayService" :style="{ justifyContent: 'center' }"> <a-space direction="horizontal" @click="restartXrayService" class="jc-center">
<a-icon type="reload"></a-icon> <a-icon type="reload"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.restartXray" }}</span> <span v-if="!isMobile">{{ i18n "pages.index.restartXray" }}</span>
</a-space> </a-space>
<a-space direction="horizontal" @click="openSelectV2rayVersion" :style="{ justifyContent: 'center' }"> <a-space direction="horizontal" @click="openSelectV2rayVersion" class="jc-center">
<a-icon type="tool"></a-icon> <a-icon type="tool"></a-icon>
<span v-if="!isMobile"> <span v-if="!isMobile">
[[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n "pages.index.xraySwitch" }}' ]] [[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n "pages.index.xraySwitch" }}' ]]
@@ -199,15 +134,15 @@
<a-col :sm="24" :lg="12"> <a-col :sm="24" :lg="12">
<a-card title='{{ i18n "menu.link" }}' hoverable> <a-card title='{{ i18n "menu.link" }}' hoverable>
<template #actions> <template #actions>
<a-space direction="horizontal" @click="openLogs()" :style="{ justifyContent: 'center' }"> <a-space direction="horizontal" @click="openLogs()" class="jc-center">
<a-icon type="bars"></a-icon> <a-icon type="bars"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span> <span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
</a-space> </a-space>
<a-space direction="horizontal" @click="openConfig" :style="{ justifyContent: 'center' }"> <a-space direction="horizontal" @click="openConfig" class="jc-center">
<a-icon type="control"></a-icon> <a-icon type="control"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.config" }}</span> <span v-if="!isMobile">{{ i18n "pages.index.config" }}</span>
</a-space> </a-space>
<a-space direction="horizontal" @click="openBackup" :style="{ justifyContent: 'center' }"> <a-space direction="horizontal" @click="openBackup" class="jc-center">
<a-icon type="cloud-server"></a-icon> <a-icon type="cloud-server"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.backup" }}</span> <span v-if="!isMobile">{{ i18n "pages.index.backup" }}</span>
</a-space> </a-space>
@@ -310,7 +245,7 @@
<template #title> <template #title>
{{ i18n "pages.index.toggleIpVisibility" }} {{ i18n "pages.index.toggleIpVisibility" }}
</template> </template>
<a-icon :type="showIp ? 'eye' : 'eye-invisible'" :style="{ fontSize: '1rem' }" @click="showIp = !showIp"></a-icon> <a-icon :type="showIp ? 'eye' : 'eye-invisible'" class="fs-1rem" @click="showIp = !showIp"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8,8] : 0"> <a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8,8] : 0">
@@ -361,8 +296,8 @@
@ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer=""> @ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
<a-collapse default-active-key="1"> <a-collapse default-active-key="1">
<a-collapse-panel key="1" header='Xray'> <a-collapse-panel key="1" header='Xray'>
<a-alert type="warning" :style="{ marginBottom: '12px', width: '100%' }" message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert> <a-alert type="warning" class="mb-12 w-100" message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert>
<a-list class="ant-version-list" bordered :style="{ width: '100%' }"> <a-list class="ant-version-list w-100" bordered>
<a-list-item class="ant-version-list-item" v-for="version, index in versionModal.versions"> <a-list-item class="ant-version-list-item" v-for="version, index in versionModal.versions">
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ version ]]</a-tag> <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-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`" @click="switchV2rayVersion(version)"></a-radio>
@@ -370,12 +305,13 @@
</a-list> </a-list>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel key="2" header='Geofiles'> <a-collapse-panel key="2" header='Geofiles'>
<a-list class="ant-version-list" bordered :style="{ width: '100%' }"> <a-list class="ant-version-list w-100" bordered>
<a-list-item class="ant-version-list-item" v-for="file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']"> <a-list-item class="ant-version-list-item" v-for="file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']">
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag> <a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag>
<a-icon type="reload" @click="updateGeofile(file)" :style="{ marginRight: '8px' }"/> <a-icon type="reload" @click="updateGeofile(file)" class="mr-8"/>
</a-list-item> </a-list-item>
</a-list> </a-list>
<div class="mt-5 d-flex justify-end"><a-button @click="updateGeofile('')">{{ i18n "pages.index.geofilesUpdateAll" }}</a-button></div>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</a-modal> </a-modal>
@@ -387,15 +323,15 @@
{{ i18n "pages.index.logs" }} {{ i18n "pages.index.logs" }}
<a-icon :spin="logModal.loading" <a-icon :spin="logModal.loading"
type="sync" type="sync"
:style="{ verticalAlign: 'middle', marginLeft: '10px' }" class="va-middle ml-10"
:disabled="logModal.loading" :disabled="logModal.loading"
@click="openLogs()"> @click="openLogs()">
</a-icon> </a-icon>
</template> </template>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item :style="{ marginRight: '0.5rem' }"> <a-form-item class="mr-05">
<a-input-group compact> <a-input-group compact>
<a-select size="small" v-model="logModal.rows" :style="{ width: '70px' }" <a-select size="small" v-model="logModal.rows" class="w-70"
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme"> @change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="10">10</a-select-option> <a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option> <a-select-option value="20">20</a-select-option>
@@ -403,7 +339,7 @@
<a-select-option value="100">100</a-select-option> <a-select-option value="100">100</a-select-option>
<a-select-option value="500">500</a-select-option> <a-select-option value="500">500</a-select-option>
</a-select> </a-select>
<a-select size="small" v-model="logModal.level" :style="{ width: '95px' }" <a-select size="small" v-model="logModal.level" class="w-95"
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme"> @change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="debug">Debug</a-select-option> <a-select-option value="debug">Debug</a-select-option>
<a-select-option value="info">Info</a-select-option> <a-select-option value="info">Info</a-select-option>
@@ -416,11 +352,53 @@
<a-form-item> <a-form-item>
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox> <a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
</a-form-item> </a-form-item>
<a-form-item :style="{ float: 'right' }"> <a-form-item style="float: right;">
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button> <a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
<div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="logModal.formattedLogs"></div> <div class="ant-input log-container" v-html="logModal.formattedLogs"></div>
</a-modal>
<a-modal id="xraylog-modal"
v-model="xraylogModal.visible"
:closable="true" @cancel="() => xraylogModal.visible = false"
:class="themeSwitcher.currentTheme"
width="80vw"
footer="">
<template slot="title">
{{ i18n "pages.index.logs" }}
<a-icon :spin="xraylogModal.loading"
type="sync"
class="va-middle ml-10"
:disabled="xraylogModal.loading"
@click="openXrayLogs()">
</a-icon>
</template>
<a-form layout="inline">
<a-form-item class="mr-05">
<a-input-group compact>
<a-select size="small" v-model="xraylogModal.rows" class="w-70"
@change="openXrayLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option>
<a-select-option value="50">50</a-select-option>
<a-select-option value="100">100</a-select-option>
<a-select-option value="500">500</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item label="Filter:">
<a-input size="small" v-model="xraylogModal.filter" @keyup.enter="openXrayLogs()"></a-input>
</a-form-item>
<a-form-item>
<a-checkbox v-model="xraylogModal.showDirect" @change="openXrayLogs()">Direct</a-checkbox>
<a-checkbox v-model="xraylogModal.showBlocked" @change="openXrayLogs()">Blocked</a-checkbox>
<a-checkbox v-model="xraylogModal.showProxy" @change="openXrayLogs()">Proxy</a-checkbox>
</a-form-item>
<a-form-item style="float: right;">
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(xraylogModal.logs?.join('\n'), 'x-ui.log')"></a-button>
</a-form-item>
</a-form>
<div class="ant-input log-container" v-html="xraylogModal.formattedLogs"></div>
</a-modal> </a-modal>
<a-modal id="backup-modal" <a-modal id="backup-modal"
v-model="backupModal.visible" v-model="backupModal.visible"
@@ -428,7 +406,7 @@
:closable="true" :closable="true"
footer="" footer=""
:class="themeSwitcher.currentTheme"> :class="themeSwitcher.currentTheme">
<a-list class="ant-backup-list" bordered :style="{ width: '100%' }"> <a-list class="ant-backup-list w-100" bordered>
<a-list-item class="ant-backup-list-item"> <a-list-item class="ant-backup-list-item">
<a-list-item-meta> <a-list-item-meta>
<template #title>{{ i18n "pages.index.exportDatabase" }}</template> <template #title>{{ i18n "pages.index.exportDatabase" }}</template>
@@ -606,6 +584,60 @@
}, },
}; };
const xraylogModal = {
visible: false,
logs: [],
rows: 20,
showDirect: true,
showBlocked: true,
showProxy: true,
loading: false,
show(logs) {
this.visible = true;
this.logs = logs;
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
},
formatLogs(logs) {
let formattedLogs = '';
logs.forEach((log, index) => {
if(index > 0) formattedLogs += '<br>';
const parts = log.split(' ');
if(parts.length === 10) {
const dateTime = `<b>${parts[0]} ${parts[1]}</b>`;
const from = `<b>${parts[3]}</b>`;
const to = `<b>${parts[5].replace(/^\/+/, "")}</b>`;
let outboundColor = '';
if (parts[9] === "b") {
outboundColor = ' style="color: #e04141;"'; //red for blocked
}
else if (parts[9] === "p") {
outboundColor = ' style="color: #3c89e8;"'; //blue for proxies
}
formattedLogs += `<span${outboundColor}>
${dateTime}
${parts[2]}
${from}
${parts[4]}
${to}
${parts.slice(6, 9).join(' ')}
</span>`;
} else {
formattedLogs += `<span>${log}</span>`;
}
});
return formattedLogs;
},
hide() {
this.visible = false;
},
};
const backupModal = { const backupModal = {
visible: false, visible: false,
show() { show() {
@@ -629,10 +661,12 @@
status: new Status(), status: new Status(),
versionModal, versionModal,
logModal, logModal,
xraylogModal,
backupModal, backupModal,
loadingTip: '{{ i18n "loading"}}', loadingTip: '{{ i18n "loading"}}',
showAlert: false, showAlert: false,
showIp: false showIp: false,
ipLimitEnable: false,
}, },
methods: { methods: {
loading(spinning, tip = '{{ i18n "loading"}}') { loading(spinning, tip = '{{ i18n "loading"}}') {
@@ -641,7 +675,7 @@
}, },
async getStatus() { async getStatus() {
try { try {
const msg = await HttpUtil.post('/server/status'); const msg = await HttpUtil.get('/panel/api/server/status');
if (msg.success) { if (msg.success) {
if (!this.loadingStates.fetched) { if (!this.loadingStates.fetched) {
this.loadingStates.fetched = true; this.loadingStates.fetched = true;
@@ -658,7 +692,7 @@
}, },
async openSelectV2rayVersion() { async openSelectV2rayVersion() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/getXrayVersion'); const msg = await HttpUtil.get('/panel/api/server/getXrayVersion');
this.loading(false); this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
@@ -675,29 +709,35 @@
onOk: async () => { onOk: async () => {
versionModal.hide(); versionModal.hide();
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}'); this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
await HttpUtil.post(`/server/installXray/${version}`); await HttpUtil.post(`/panel/api/server/installXray/${version}`);
this.loading(false); this.loading(false);
}, },
}); });
}, },
updateGeofile(fileName) { updateGeofile(fileName) {
const isSingleFile = !!fileName;
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.index.geofileUpdateDialog" }}', title: '{{ i18n "pages.index.geofileUpdateDialog" }}',
content: '{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName), content: isSingleFile
? '{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName)
: '{{ i18n "pages.index.geofilesUpdateDialogDesc" }}',
okText: '{{ i18n "confirm"}}', okText: '{{ i18n "confirm"}}',
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
versionModal.hide(); versionModal.hide();
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}'); this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
await HttpUtil.post(`/server/updateGeofile/${fileName}`); const url = isSingleFile
? `/panel/api/server/updateGeofile/${fileName}`
: `/panel/api/server/updateGeofile`;
await HttpUtil.post(url);
this.loading(false); this.loading(false);
}, },
}); });
}, },
async stopXrayService() { async stopXrayService() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/stopXrayService'); const msg = await HttpUtil.post('/panel/api/server/stopXrayService');
this.loading(false); this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
@@ -705,7 +745,7 @@
}, },
async restartXrayService() { async restartXrayService() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/restartXrayService'); const msg = await HttpUtil.post('/panel/api/server/restartXrayService');
this.loading(false); this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
@@ -713,7 +753,7 @@
}, },
async openLogs(){ async openLogs(){
logModal.loading = true; logModal.loading = true;
const msg = await HttpUtil.post('server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog}); const msg = await HttpUtil.post('/panel/api/server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog});
if (!msg.success) { if (!msg.success) {
return; return;
} }
@@ -721,9 +761,19 @@
await PromiseUtil.sleep(500); await PromiseUtil.sleep(500);
logModal.loading = false; logModal.loading = false;
}, },
async openXrayLogs(){
xraylogModal.loading = true;
const msg = await HttpUtil.post('/panel/api/server/xraylogs/'+xraylogModal.rows,{filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy});
if (!msg.success) {
return;
}
xraylogModal.show(msg.obj);
await PromiseUtil.sleep(500);
xraylogModal.loading = false;
},
async openConfig() { async openConfig() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/getConfigJson'); const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
this.loading(false); this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
@@ -734,7 +784,7 @@
backupModal.show(); backupModal.show();
}, },
exportDatabase() { exportDatabase() {
window.location = basePath + 'server/getDb'; window.location = basePath + 'panel/api/server/getDb';
}, },
importDatabase() { importDatabase() {
const fileInput = document.createElement('input'); const fileInput = document.createElement('input');
@@ -747,7 +797,7 @@
formData.append('db', dbFile); formData.append('db', dbFile);
backupModal.hide(); backupModal.hide();
this.loading(true); this.loading(true);
const uploadMsg = await HttpUtil.post('server/importDB', formData, { const uploadMsg = await HttpUtil.post('/panel/api/server/importDB', formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
} }
@@ -773,6 +823,12 @@
if (window.location.protocol !== "https:") { if (window.location.protocol !== "https:") {
this.showAlert = true; this.showAlert = true;
} }
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
if (msg.success) {
this.ipLimitEnable = msg.obj.ipLimitEnable;
}
while (true) { while (true) {
try { try {
await this.getStatus(); await this.getStatus();

View File

@@ -1,462 +1,10 @@
{{ template "page/head_start" .}} {{ template "page/head_start" .}}
<style>
html * {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1 {
text-align: center;
/*margin: 20px 0 50px 0;*/
height: 110px;
}
.ant-form-item-children .ant-btn,
.ant-input {
height: 50px;
border-radius: 30px;
}
.ant-input-group-addon {
border-radius: 0 30px 30px 0;
width: 50px;
font-size: 18px;
}
.ant-input-affix-wrapper .ant-input-prefix {
left: 23px;
}
.ant-input-affix-wrapper .ant-input:not(:first-child) {
padding-left: 50px;
}
.centered {
display: flex;
text-align: center;
align-items: center;
justify-content: center;
width: 100%;
}
.title {
font-size: 2rem;
margin-block-end: 2rem;
}
.title b {
font-weight: bold !important;
}
#app {
overflow: hidden;
}
#login {
animation: charge 0.5s both;
background-color: #fff;
border-radius: 2rem;
padding: 4rem 3rem;
transition: all 0.3s;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
#login:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
}
@keyframes charge {
from {
transform: translateY(5rem);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.under {
background-color: #c7ebe2;
z-index: 0;
}
.dark .under {
background-color: var(--dark-color-login-wave);
}
.dark #login {
background-color: var(--dark-color-surface-100);
}
.dark h1 {
color: rgba(255, 255, 255);
}
.ant-btn-primary-login {
width: 100%;
}
.ant-btn-primary-login:focus,
.ant-btn-primary-login:hover {
color: #fff;
background-color: #006655;
border-color: #006655;
background-image: linear-gradient(270deg,
rgba(123, 199, 77, 0) 30%,
#009980,
rgba(123, 199, 77, 0) 100%);
background-repeat: no-repeat;
animation: ma-bg-move ease-in-out 5s infinite;
background-position-x: -500px;
width: 95%;
animation-delay: -0.5s;
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045);
}
.ant-btn-primary-login.active,
.ant-btn-primary-login:active {
color: #fff;
background-color: #006655;
border-color: #006655;
}
@keyframes ma-bg-move {
0% {
background-position: -500px 0;
}
50% {
background-position: 1000px 0;
}
100% {
background-position: 1000px 0;
}
}
.wave-btn-bg {
position: relative;
border-radius: 25px;
width: 100%;
transition: all 0.3s cubic-bezier(.645, .045, .355, 1);
}
.dark .wave-btn-bg {
color: #fff;
position: relative;
background-color: #0a7557;
border: 2px double transparent;
background-origin: border-box;
background-clip: padding-box, border-box;
background-size: 300%;
width: 100%;
z-index: 1;
}
.dark .wave-btn-bg:hover {
animation: wave-btn-tara 4s ease infinite;
}
.dark .wave-btn-bg-cl {
background-image: linear-gradient(rgba(13, 14, 33, 0), rgba(13, 14, 33, 0)),
radial-gradient(circle at left top, #006655, #009980, #006655) !important;
border-radius: 3em;
}
.dark .wave-btn-bg-cl:hover {
width: 95%;
}
.dark .wave-btn-bg-cl:before {
position: absolute;
content: "";
top: -5px;
left: -5px;
bottom: -5px;
right: -5px;
z-index: -1;
background: inherit;
background-size: inherit;
border-radius: 4em;
opacity: 0;
transition: 0.5s;
}
.dark .wave-btn-bg-cl:hover::before {
opacity: 1;
filter: blur(20px);
animation: wave-btn-tara 8s linear infinite;
}
@keyframes wave-btn-tara {
to {
background-position: 300%;
}
}
.dark .ant-btn-primary-login {
font-size: 14px;
color: #fff;
text-align: center;
background-image: linear-gradient(rgba(13, 14, 33, 0.45),
rgba(13, 14, 33, 0.35));
border-radius: 2rem;
border: none;
outline: none;
background-color: transparent;
height: 46px;
position: relative;
white-space: nowrap;
cursor: pointer;
touch-action: manipulation;
padding: 0 15px;
width: 100%;
animation: none;
background-position-x: 0;
box-shadow: none;
}
.waves-header {
position: fixed;
width: 100%;
text-align: center;
background-color: #dbf5ed;
color: white;
z-index: -1;
}
.dark .waves-header {
background-color: var(--dark-color-login-background);
}
.waves-inner-header {
height: 50vh;
width: 100%;
margin: 0;
padding: 0;
}
.waves {
position: relative;
width: 100%;
height: 15vh;
margin-bottom: -8px;
/*Fix for safari gap*/
min-height: 100px;
max-height: 150px;
}
.parallax>use {
animation: move-forever 25s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite;
}
.dark .parallax>use {
fill: var(--dark-color-login-wave);
}
.parallax>use:nth-child(1) {
animation-delay: -2s;
animation-duration: 4s;
opacity: 0.2;
}
.parallax>use:nth-child(2) {
animation-delay: -3s;
animation-duration: 7s;
opacity: 0.4;
}
.parallax>use:nth-child(3) {
animation-delay: -4s;
animation-duration: 10s;
opacity: 0.6;
}
.parallax>use:nth-child(4) {
animation-delay: -5s;
animation-duration: 13s;
}
@keyframes move-forever {
0% {
transform: translate3d(-90px, 0, 0);
}
100% {
transform: translate3d(85px, 0, 0);
}
}
@media (max-width: 768px) {
.waves {
height: 40px;
min-height: 40px;
}
}
@media (max-width: 1024px) {
.title {
font-size: 1.4rem;
}
}
.words-wrapper {
width: 100%;
display: inline-block;
position: relative;
text-align: center;
}
.words-wrapper b {
width: 100%;
display: inline-block;
position: absolute;
left: 0;
top: 0;
}
.words-wrapper b.is-visible {
position: relative;
}
.headline.zoom .words-wrapper {
-webkit-perspective: 300px;
-moz-perspective: 300px;
perspective: 300px;
}
.headline {
display: flex;
justify-content: center;
align-items: center;
}
.headline.zoom b {
opacity: 0;
}
.headline.zoom b.is-visible {
opacity: 1;
-webkit-animation: zoom-in 0.8s;
-moz-animation: zoom-in 0.8s;
animation: cubic-bezier(0.215, 0.610, 0.355, 1.000) zoom-in 0.8s;
}
.headline.zoom b.is-hidden {
-webkit-animation: zoom-out 0.8s;
-moz-animation: zoom-out 0.8s;
animation: cubic-bezier(0.215, 0.610, 0.355, 1.000) zoom-out 0.4s;
}
@-webkit-keyframes zoom-in {
0% {
opacity: 0;
-webkit-transform: translateZ(100px);
}
100% {
opacity: 1;
-webkit-transform: translateZ(0);
}
}
@-moz-keyframes zoom-in {
0% {
opacity: 0;
-moz-transform: translateZ(100px);
}
100% {
opacity: 1;
-moz-transform: translateZ(0);
}
}
@keyframes zoom-in {
0% {
opacity: 0;
-webkit-transform: translateZ(100px);
-moz-transform: translateZ(100px);
-ms-transform: translateZ(100px);
-o-transform: translateZ(100px);
transform: translateZ(100px);
}
100% {
opacity: 1;
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
-o-transform: translateZ(0);
transform: translateZ(0);
}
}
@-webkit-keyframes zoom-out {
0% {
opacity: 1;
-webkit-transform: translateZ(0);
}
100% {
opacity: 0;
-webkit-transform: translateZ(-100px);
}
}
@-moz-keyframes zoom-out {
0% {
opacity: 1;
-moz-transform: translateZ(0);
}
100% {
opacity: 0;
-moz-transform: translateZ(-100px);
}
}
@keyframes zoom-out {
0% {
opacity: 1;
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
-o-transform: translateZ(0);
transform: translateZ(0);
}
100% {
opacity: 0;
-webkit-transform: translateZ(-100px);
-moz-transform: translateZ(-100px);
-ms-transform: translateZ(-100px);
-o-transform: translateZ(-100px);
transform: translateZ(-100px);
}
}
.setting-section {
position: absolute;
top: 0;
right: 0;
padding: 22px;
}
.ant-space-item .ant-switch {
margin: 2px 0 4px;
}
</style>
{{ template "page/head_end" .}} {{ template "page/head_end" .}}
{{ template "page/body_start" .}} {{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme"> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
<transition name="list" appear> <transition name="list" appear>
<a-layout-content class="under" :style="{ minHeight: '0' }"> <a-layout-content class="under min-h-100vh">
<div class="waves-header"> <div class="waves-header">
<div class="waves-inner-header"></div> <div class="waves-inner-header"></div>
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" <svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
@@ -472,71 +20,80 @@
</g> </g>
</svg> </svg>
</div> </div>
<a-row type="flex" justify="center" align="middle" :style="{ height: '100%', overflow: 'auto', overflowX: 'hidden' }"> <a-row type="flex" justify="center" align="middle" class="h-100 overflow-hidden-auto">
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" :style="{ margin: '3rem 0' }"> <a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" class="my-3rem">
<div class="setting-section"> <template v-if="!loadingStates.fetched">
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}' placement="bottomRight" trigger="click"> <div class="text-center">
<template slot="content"> <a-spin size="large" />
<a-space direction="vertical" :size="10"> </div>
<a-theme-switch-login></a-theme-switch-login> </template>
<span>{{ i18n "pages.settings.language" }}</span> <template v-else>
<a-select ref="selectLang" :style="{ width: '100%' }" v-model="lang" @change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme"> <div class="setting-section">
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages"> <a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}'
<span role="img" aria-label="l.name" v-text="l.icon"></span> placement="bottomRight" trigger="click">
&nbsp;&nbsp;<span v-text="l.name"></span> <template slot="content">
</a-select-option> <a-space direction="vertical" :size="10">
</a-select> <a-theme-switch-login></a-theme-switch-login>
</a-space> <span>{{ i18n "pages.settings.language" }}</span>
</template> <a-select ref="selectLang" class="w-100" v-model="lang"
<a-button shape="circle" icon="setting"></a-button> @change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
</a-popover> <a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
</div> <span role="img" aria-label="l.name" v-text="l.icon"></span>
<a-row type="flex" justify="center"> &nbsp;&nbsp;<span v-text="l.name"></span>
<a-col :style="{ width: '100%' }"> </a-select-option>
<h2 class="title headline zoom"> </a-select>
<span class="words-wrapper"> </a-space>
<b class="is-visible">{{ i18n "pages.login.hello" }}</b> </template>
<b>{{ i18n "pages.login.title" }}</b> <a-button shape="circle" icon="setting"></a-button>
</span> </a-popover>
</h2> </div>
</a-col> <a-row type="flex" justify="center">
</a-row> <a-col :style="{ width: '100%' }">
<a-row type="flex" justify="center"> <h2 class="title headline zoom">
<a-col span="24"> <span class="words-wrapper">
<a-form> <b class="is-visible">{{ i18n "pages.login.hello" }}</b>
<a-space direction="vertical" size="middle"> <b>{{ i18n "pages.login.title" }}</b>
<a-form-item> </span>
<a-input autocomplete="username" name="username" v-model.trim="user.username" </h2>
placeholder='{{ i18n "username" }}' @keydown.enter.native="login" autofocus> </a-col>
<a-icon slot="prefix" type="user" :style="{ fontSize: '1rem' }"></a-icon> </a-row>
</a-input> <a-row type="flex" justify="center">
</a-form-item> <a-col span="24">
<a-form-item> <a-form @submit.prevent="login">
<a-input-password autocomplete="password" name="password" v-model.trim="user.password" <a-space direction="vertical" size="middle">
placeholder='{{ i18n "password" }}' @keydown.enter.native="login"> <a-form-item>
<a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon> <a-input autocomplete="username" name="username" v-model.trim="user.username"
</a-input-password> placeholder='{{ i18n "username" }}' autofocus required>
</a-form-item> <a-icon slot="prefix" type="user" class="fs-1rem"></a-icon>
<a-form-item v-if="twoFactorEnable"> </a-input>
<a-input autocomplete="totp" name="twoFactorCode" v-model.trim="user.twoFactorCode" </a-form-item>
placeholder='{{ i18n "twoFactorCode" }}' @keydown.enter.native="login"> <a-form-item>
<a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon> <a-input-password autocomplete="password" name="password" v-model.trim="user.password"
</a-input> placeholder='{{ i18n "password" }}' required>
</a-form-item> <a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
<a-form-item> </a-input-password>
<a-row justify="center" class="centered"> </a-form-item>
<div :style="{ height: '50px', marginTop: '1rem', ...loading ? { width: '52px' } : { display: 'inline-block' } }" class="wave-btn-bg wave-btn-bg-cl"> <a-form-item v-if="twoFactorEnable">
<a-button class="ant-btn-primary-login" type="primary" :loading="loading" @click="login" <a-input autocomplete="one-time-code" name="twoFactorCode" v-model.trim="user.twoFactorCode"
:icon="loading ? 'poweroff' : undefined"> placeholder='{{ i18n "twoFactorCode" }}' required>
[[ loading ? '' : '{{ i18n "login" }}' ]] <a-icon slot="prefix" type="key" class="fs-1rem"></a-icon>
</a-button> </a-input>
</div> </a-form-item>
</a-row> <a-form-item>
</a-form-item> <a-row justify="center" class="centered">
</a-space> <div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem" :style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
</a-form> <a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
</a-col> :icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
</a-row> [[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
</a-button>
</div>
</a-row>
</a-form-item>
</a-space>
</a-form>
</a-col>
</a-row>
</template>
</a-col> </a-col>
</a-row> </a-row>
</a-layout-content> </a-layout-content>
@@ -550,7 +107,10 @@
el: '#app', el: '#app',
data: { data: {
themeSwitcher, themeSwitcher,
loading: false, loadingStates: {
fetched: false,
spinning: false
},
user: { user: {
username: "", username: "",
password: "", password: "",
@@ -565,19 +125,23 @@
}, },
methods: { methods: {
async login() { async login() {
this.loading = true; this.loadingStates.spinning = true;
const msg = await HttpUtil.post('/login', this.user); const msg = await HttpUtil.post('/login', this.user);
this.loading = false;
if (msg.success) { if (msg.success) {
location.href = basePath + 'panel/'; location.href = basePath + 'panel/';
} }
this.loadingStates.spinning = false;
}, },
async getTwoFactorEnable() { async getTwoFactorEnable() {
this.loading = true;
const msg = await HttpUtil.post('/getTwoFactorEnable'); const msg = await HttpUtil.post('/getTwoFactorEnable');
this.loading = false;
if (msg.success) { if (msg.success) {
this.twoFactorEnable = msg.obj; this.twoFactorEnable = msg.obj;
this.loadingStates.fetched = true;
return msg.obj; return msg.obj;
} }
}, },

View File

@@ -39,7 +39,7 @@
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="app.subSettings.enable"> <a-form-item v-if="app.subSettings?.enable">
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">

View File

@@ -47,7 +47,7 @@
} }
this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]); this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]);
} else { } else {
this.addClient(this.inbound.protocol, this.clients); this.addClient(this.inbound, this.clients);
} }
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email); this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm; this.confirm = confirm;
@@ -59,12 +59,12 @@
default: return client.id; default: return client.id;
} }
}, },
addClient(protocol, clients) { addClient(inbound, clients) {
switch (protocol) { switch (inbound.protocol) {
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.VMESS()); case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.VMESS());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS()); case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan()); case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method)); case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil.randomShadowsocksPassword(inbound.settings.method)));
default: return null; default: return null;
} }
}, },
@@ -121,7 +121,7 @@
}, },
methods: { methods: {
async getDBClientIps(email) { async getDBClientIps(email) {
const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`); const msg = await HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`);
if (!msg.success) { if (!msg.success) {
document.getElementById("clientIPs").value = msg.obj; document.getElementById("clientIPs").value = msg.obj;
return; return;
@@ -139,7 +139,7 @@
}, },
async clearDBClientIps(email) { async clearDBClientIps(email) {
try { try {
const msg = await HttpUtil.post(`/panel/inbound/clearClientIps/${email}`); const msg = await HttpUtil.post(`/panel/api/inbounds/clearClientIps/${email}`);
if (!msg.success) { if (!msg.success) {
return; return;
} }
@@ -156,7 +156,7 @@
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
iconElement.disabled = true; iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email); const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + email);
if (msg.success) { if (msg.success) {
this.clientModal.clientStats.up = 0; this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0; this.clientModal.clientStats.down = 0;

View File

@@ -101,6 +101,12 @@
{{ i18n "security" }} {{ i18n "security" }}
<a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag> <a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag>
<br /> <br />
<td>Authentication</td>
<a-tag :color="inbound.settings.selectedAuth ? 'green' : 'red'">[[ inbound.settings.selectedAuth ? inbound.settings.selectedAuth : '' ]]</a-tag>
<br />
{{ i18n "encryption" }}
<a-tag :color="inbound.settings.encryption ? 'green' : 'red'">[[ inbound.settings.encryption ? inbound.settings.encryption : '' ]]</a-tag>
<br />
<template v-if="inbound.stream.security != 'none'"> <template v-if="inbound.stream.security != 'none'">
{{ i18n "domainName" }} {{ i18n "domainName" }}
<a-tag v-if="inbound.serverName" :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> <a-tag v-if="inbound.serverName" :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
@@ -185,6 +191,44 @@
<a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up) ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag> <a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up) ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
</td> </td>
</tr> </tr>
<tr>
<td>{{ i18n "pages.inbounds.createdAt" }}</td>
<td>
<template v-if="infoModal.clientSettings && infoModal.clientSettings.created_at">
<template v-if="app.datepicker === 'gregorian'">
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.created_at) ]]</a-tag>
</template>
<template v-else>
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.created_at)) ]]</a-tag>
</template>
</template>
<template v-else>
<a-tag>-</a-tag>
</template>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.updatedAt" }}</td>
<td>
<template v-if="infoModal.clientSettings && infoModal.clientSettings.updated_at">
<template v-if="app.datepicker === 'gregorian'">
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.updated_at) ]]</a-tag>
</template>
<template v-else>
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.updated_at)) ]]</a-tag>
</template>
</template>
<template v-else>
<a-tag>-</a-tag>
</template>
</td>
</tr>
<tr>
<td>{{ i18n "lastOnline" }}</td>
<td>
<a-tag>[[ app.formatLastOnline(infoModal.clientSettings && infoModal.clientSettings.email ? infoModal.clientSettings.email : '') ]]</a-tag>
</td>
</tr>
<tr v-if="infoModal.clientSettings.comment"> <tr v-if="infoModal.clientSettings.comment">
<td>{{ i18n "comment" }}</td> <td>{{ i18n "comment" }}</td>
<td> <td>
@@ -199,7 +243,7 @@
<a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag> <a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag>
</td> </td>
</tr> </tr>
<tr v-if="app.ipLimitEnable"> <tr v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td> <td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
<td> <td>
<a-tag>[[ infoModal.clientIps ]]</a-tag> <a-tag>[[ infoModal.clientIps ]]</a-tag>
@@ -310,7 +354,7 @@
<code>[[ link.link ]]</code> <code>[[ link.link ]]</code>
</tr-info-row> </tr-info-row>
</template> </template>
<table v-if="inbound.protocol == Protocols.DOKODEMO" class="tr-info-table"> <table v-if="inbound.protocol == Protocols.TUNNEL" class="tr-info-table">
<tr> <tr>
<th>{{ i18n "pages.inbounds.targetAddress" }}</th> <th>{{ i18n "pages.inbounds.targetAddress" }}</th>
<th>{{ i18n "pages.inbounds.destinationPort" }}</th> <th>{{ i18n "pages.inbounds.destinationPort" }}</th>
@@ -332,7 +376,7 @@
</td> </td>
</tr> </tr>
</table> </table>
<table v-if="dbInbound.isSocks" class="tr-info-table"> <table v-if="dbInbound.isMixed" class="tr-info-table">
<tr> <tr>
<th>{{ i18n "password" }} Auth</th> <th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th> <th>{{ i18n "pages.inbounds.enable" }} udp</th>
@@ -448,7 +492,7 @@
</a-modal> </a-modal>
<script> <script>
function refreshIPs(email) { function refreshIPs(email) {
return HttpUtil.post(`/panel/inbound/clientIps/${email}`).then((msg) => { return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
if (msg.success) { if (msg.success) {
try { try {
return JSON.parse(msg.obj).join(', '); return JSON.parse(msg.obj).join(', ');
@@ -569,7 +613,7 @@
}); });
}, },
clearClientIps() { clearClientIps() {
HttpUtil.post(`/panel/inbound/clearClientIps/${this.infoModal.clientStats.email}`) HttpUtil.post(`/panel/api/inbounds/clearClientIps/${this.infoModal.clientStats.email}`)
.then((msg) => { .then((msg) => {
if (!msg.success) { if (!msg.success) {
return; return;

View File

@@ -1,9 +1,7 @@
{{define "modals/inboundModal"}} {{define "modals/inboundModal"}}
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" <a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" :dialog-style="{ top: '20px' }"
:dialog-style="{ top: '20px' }" @ok="inModal.ok" @ok="inModal.ok" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false" :class="themeSwitcher.currentTheme" :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
:class="themeSwitcher.currentTheme"
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/inbound"}} {{template "form/inbound"}}
</a-modal> </a-modal>
<script> <script>
@@ -20,7 +18,7 @@
ok() { ok() {
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound); ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
}, },
show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => {}, isEdit = false }) { show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => { }, isEdit = false }) {
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;
if (inbound) { if (inbound) {
@@ -41,7 +39,7 @@
inModal.visible = false; inModal.visible = false;
inModal.loading(false); inModal.loading(false);
}, },
loading(loading=true) { loading(loading = true) {
inModal.confirmLoading = loading; inModal.confirmLoading = loading;
}, },
}; };
@@ -104,8 +102,10 @@
} }
}, },
SSMethodChange() { SSMethodChange() {
this.inModal.inbound.settings.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
if (this.inModal.inbound.isSSMultiUser) { if (this.inModal.inbound.isSSMultiUser) {
if (this.inModal.inbound.settings.shadowsockses.length ==0){ if (this.inModal.inbound.settings.shadowsockses.length == 0) {
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()]; this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
} }
if (!this.inModal.inbound.isSS2022) { if (!this.inModal.inbound.isSS2022) {
@@ -117,8 +117,11 @@
client.method = ""; client.method = "";
}) })
} }
this.inModal.inbound.settings.shadowsockses.forEach(client => {
client.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
})
} else { } else {
if (this.inModal.inbound.settings.shadowsockses.length > 0){ if (this.inModal.inbound.settings.shadowsockses.length > 0) {
this.inModal.inbound.settings.shadowsockses = []; this.inModal.inbound.settings.shadowsockses = [];
} }
} }
@@ -129,16 +132,75 @@
}, },
async getNewX25519Cert() { async getNewX25519Cert() {
inModal.loading(true); inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewX25519Cert'); const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
inModal.loading(false); inModal.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
} }
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey; inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey; inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
},
clearX25519Cert() {
this.inbound.stream.reality.privateKey = '';
this.inbound.stream.reality.settings.publicKey = '';
},
async getNewmldsa65() {
inModal.loading(true);
const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
inModal.loading(false);
if (!msg.success) {
return;
}
inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed;
inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify;
},
clearMldsa65() {
this.inbound.stream.reality.mldsa65Seed = '';
this.inbound.stream.reality.settings.mldsa65Verify = '';
},
async getNewEchCert() {
inModal.loading(true);
const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni });
inModal.loading(false);
if (!msg.success) {
return;
}
inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys;
inModal.inbound.stream.tls.settings.echConfigList = msg.obj.echConfigList;
},
clearEchCert() {
this.inbound.stream.tls.echServerKeys = '';
this.inbound.stream.tls.settings.echConfigList = '';
},
async getNewVlessEnc() {
inModal.loading(true);
const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
inModal.loading(false);
if (!msg.success) {
return;
}
const auths = msg.obj.auths || [];
const selected = inModal.inbound.settings.selectedAuth;
const block = auths.find(a => a.label === selected);
if (!block) {
console.error("No auth block for", selected);
return;
}
inModal.inbound.settings.decryption = block.decryption;
inModal.inbound.settings.encryption = block.encryption;
},
clearVlessEnc() {
this.inbound.settings.decryption = 'none';
this.inbound.settings.encryption = 'none';
this.inbound.settings.selectedAuth = undefined;
} }
}, },
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -21,7 +21,7 @@
</a-space> </a-space>
</template> </template>
<tr-qr-modal class="qr-modal"> <tr-qr-modal class="qr-modal">
<template v-if="app.subSettings.enable && qrModal.subId"> <template v-if="app.subSettings?.enable && qrModal.subId">
<tr-qr-box class="qr-box"> <tr-qr-box class="qr-box">
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag> <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 class="qr-bg-sub">
@@ -151,7 +151,7 @@
methods: { methods: {
async getStatus() { async getStatus() {
try { try {
const msg = await HttpUtil.post('/server/status'); const msg = await HttpUtil.get('/panel/api/server/status');
if (msg.success) { if (msg.success) {
this.serverStatus = msg.obj; this.serverStatus = msg.obj;
} }

View File

@@ -1,11 +1,6 @@
{{define "modals/ruleModal"}} {{define "modals/ruleModal"}}
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok" :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme"> <a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok" :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='Domain Matcher'>
<a-select v-model="ruleModal.rule.domainMatcher" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="dm in ['','hybrid','linear']" :value="dm">[[ dm ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
@@ -14,7 +9,7 @@
</template> Source IPs <a-icon type="question-circle"></a-icon> </template> Source IPs <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="ruleModal.rule.source"></a-input> <a-input v-model.trim="ruleModal.rule.sourceIP" placeholder="e.g. 0.0.0.0/8, fc00::/7, geoip:ir"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
@@ -24,7 +19,17 @@
</template> Source Port <a-icon type="question-circle"></a-icon> </template> Source Port <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="ruleModal.rule.sourcePort"></a-input> <a-input v-model.trim="ruleModal.rule.sourcePort" placeholder="e.g. 53,443,1000-2000"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template> VLESS Route <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="ruleModal.rule.vlessRoute" placeholder="e.g. 53,443,1000-2000"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Network'> <a-form-item label='Network'>
<a-select v-model="ruleModal.rule.network" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="ruleModal.rule.network" :dropdown-class-name="themeSwitcher.currentTheme">
@@ -57,7 +62,7 @@
</template> IP <a-icon type="question-circle"></a-icon> </template> IP <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="ruleModal.rule.ip"></a-input> <a-input v-model.trim="ruleModal.rule.ip" placeholder="e.g. 0.0.0.0/8, fc00::/7, geoip:ir"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
@@ -67,7 +72,7 @@
</template> Domain <a-icon type="question-circle"></a-icon> </template> Domain <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="ruleModal.rule.domain"></a-input> <a-input v-model.trim="ruleModal.rule.domain" placeholder="e.g. google.com, geosite:cn"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
@@ -77,7 +82,7 @@
</template> User <a-icon type="question-circle"></a-icon> </template> User <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="ruleModal.rule.user"></a-input> <a-input v-model.trim="ruleModal.rule.user" placeholder="e.g. email address"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
@@ -87,7 +92,7 @@
</template> Port <a-icon type="question-circle"></a-icon> </template> Port <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="ruleModal.rule.port"></a-input> <a-input v-model.trim="ruleModal.rule.port" placeholder="e.g. 53,443,1000-2000"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Inbound Tags'> <a-form-item label='Inbound Tags'>
<a-select v-model="ruleModal.rule.inboundTag" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="ruleModal.rule.inboundTag" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme">
@@ -123,13 +128,13 @@
confirm: null, confirm: null,
rule: { rule: {
type: "field", type: "field",
domainMatcher: "",
domain: "", domain: "",
ip: "", ip: "",
port: "", port: "",
sourcePort: "", sourcePort: "",
vlessRoute: "",
network: "", network: "",
source: "", sourceIP: "",
user: "", user: "",
inboundTag: [], inboundTag: [],
protocol: [], protocol: [],
@@ -157,13 +162,13 @@
this.confirm = confirm; this.confirm = confirm;
this.visible = true; this.visible = true;
if (isEdit) { if (isEdit) {
this.rule.domainMatcher = rule.domainMatcher;
this.rule.domain = rule.domain ? rule.domain.join(',') : []; this.rule.domain = rule.domain ? rule.domain.join(',') : [];
this.rule.ip = rule.ip ? rule.ip.join(',') : []; this.rule.ip = rule.ip ? rule.ip.join(',') : [];
this.rule.port = rule.port; this.rule.port = rule.port;
this.rule.sourcePort = rule.sourcePort; this.rule.sourcePort = rule.sourcePort;
this.rule.vlessRoute = rule.vlessRoute;
this.rule.network = rule.network; this.rule.network = rule.network;
this.rule.source = rule.source ? rule.source.join(',') : []; this.rule.sourceIP = rule.sourceIP ? rule.sourceIP.join(',') : [];
this.rule.user = rule.user ? rule.user.join(',') : []; this.rule.user = rule.user ? rule.user.join(',') : [];
this.rule.inboundTag = rule.inboundTag; this.rule.inboundTag = rule.inboundTag;
this.rule.protocol = rule.protocol; this.rule.protocol = rule.protocol;
@@ -172,13 +177,13 @@
this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : ""; this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : "";
} else { } else {
this.rule = { this.rule = {
domainMatcher: "",
domain: "", domain: "",
ip: "", ip: "",
port: "", port: "",
sourcePort: "", sourcePort: "",
vlessRoute: "",
network: "", network: "",
source: "", sourceIP: "",
user: "", user: "",
inboundTag: [], inboundTag: [],
protocol: [], protocol: [],
@@ -214,13 +219,13 @@
rule = {}; rule = {};
newRule = {}; newRule = {};
rule.type = "field"; rule.type = "field";
rule.domainMatcher = value.domainMatcher;
rule.domain = value.domain.length > 0 ? value.domain.split(',') : []; rule.domain = value.domain.length > 0 ? value.domain.split(',') : [];
rule.ip = value.ip.length > 0 ? value.ip.split(',') : []; rule.ip = value.ip.length > 0 ? value.ip.split(',') : [];
rule.port = value.port; rule.port = value.port;
rule.sourcePort = value.sourcePort; rule.sourcePort = value.sourcePort;
rule.vlessRoute = value.vlessRoute;
rule.network = value.network; rule.network = value.network;
rule.source = value.source.length > 0 ? value.source.split(',') : []; rule.sourceIP = value.sourceIP.length > 0 ? value.sourceIP.split(',') : [];
rule.user = value.user.length > 0 ? value.user.split(',') : []; rule.user = value.user.length > 0 ? value.user.split(',') : [];
rule.inboundTag = value.inboundTag; rule.inboundTag = value.inboundTag;
rule.protocol = value.protocol; rule.protocol = value.protocol;

View File

@@ -1,67 +1,8 @@
{{ template "page/head_start" .}} {{ template "page/head_start" .}}
<style>
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
@media (max-width: 768px) {
.ant-tabs-nav .ant-tabs-tab {
margin: 0;
padding: 12px .5rem;
}
}
.ant-tabs-bar {
margin: 0;
}
.ant-list-item {
display: block;
}
.alert-msg {
color: rgb(194, 117, 18);
font-weight: normal;
font-size: 16px;
padding: .5rem 1rem;
text-align: center;
background: rgb(255 145 0 / 15%);
margin: 1.5rem 2.5rem 0rem;
border-radius: .5rem;
transition: all 0.5s;
animation: signal 3s cubic-bezier(0.18, 0.89, 0.32, 1.28) infinite;
}
.alert-msg:hover {
cursor: default;
transition-duration: .3s;
animation: signal 0.9s ease infinite;
}
@keyframes signal {
0% {
box-shadow: 0 0 0 0 rgba(194, 118, 18, 0.5);
}
50% {
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
}
100% {
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
}
}
.alert-msg>i {
color: inherit;
font-size: 24px;
}
.dark .ant-input-password-icon {
color: var(--dark-color-text-primary);
}
.ant-collapse-content-box .ant-alert {
margin-block-end: 12px;
}
</style>
{{ template "page/head_end" .}} {{ template "page/head_end" .}}
{{ template "page/body_start" .}} {{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme"> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' settings-page'">
<a-sidebar></a-sidebar> <a-sidebar></a-sidebar>
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content> <a-layout-content>
@@ -207,7 +148,7 @@
settings: { settings: {
domainStrategy: "AsIs", domainStrategy: "AsIs",
noises: [ noises: [
{ type: "rand", packet: "10-20", delay: "10-16" }, { type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" },
], ],
}, },
}, },
@@ -397,7 +338,7 @@
} }
}, },
addNoise() { addNoise() {
const newNoise = { type: "rand", packet: "10-20", delay: "10-16" }; const newNoise = { type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" };
this.noisesArray = [...this.noisesArray, newNoise]; this.noisesArray = [...this.noisesArray, newNoise];
}, },
removeNoise(index) { removeNoise(index) {
@@ -420,6 +361,11 @@
updatedNoises[index] = { ...updatedNoises[index], delay: value }; updatedNoises[index] = { ...updatedNoises[index], delay: value };
this.noisesArray = updatedNoises; this.noisesArray = updatedNoises;
}, },
updateNoiseApplyTo(index, value) {
const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], applyTo: value };
this.noisesArray = updatedNoises;
},
}, },
computed: { computed: {
fragment: { fragment: {

View File

@@ -90,6 +90,18 @@
placeholder="10-20"></a-input> placeholder="10-20"></a-input>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>ApplyTo</template>
<template #control>
<a-select :value="noise.applyTo" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme"
@change="(value) => updateNoiseApplyTo(index, value)">
<a-select-option :value="p" :label="p" v-for="p in ['ip', 'ipv4', 'ipv6']" :key="p">
<span>[[ p ]]</span>
</a-select-option>
</a-select>
</template>
</a-setting-list-item>
<a-space direction="horizontal" :style="{ padding: '10px 20px' }"> <a-space direction="horizontal" :style="{ padding: '10px 20px' }">
<a-button v-if="noisesArray.length > 1" type="danger" <a-button v-if="noisesArray.length > 1" type="danger"
@click="removeNoise(index)">Remove</a-button> @click="removeNoise(index)">Remove</a-button>

View File

@@ -67,18 +67,22 @@
</template> </template>
<template slot="info" slot-scope="text, rule, index"> <template slot="info" slot-scope="text, rule, index">
<a-popover placement="bottomRight" <a-popover placement="bottomRight"
v-if="(rule.source+rule.sourcePort+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0" v-if="(rule.sourceIP+rule.sourcePort+rule.vlessRoute+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0"
:overlay-class-name="themeSwitcher.currentTheme" trigger="click"> :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
<template slot="content"> <template slot="content">
<table cellpadding="2" :style="{ maxWidth: '300px' }"> <table cellpadding="2" :style="{ maxWidth: '300px' }">
<tr v-if="rule.source"> <tr v-if="rule.sourceIP">
<td>Source</td> <td>Source IP</td>
<td><a-tag color="blue" v-for="r in rule.source.split(',')">[[ r ]]</a-tag></td> <td><a-tag color="blue" v-for="r in rule.sourceIP.split(',')">[[ r ]]</a-tag></td>
</tr> </tr>
<tr v-if="rule.sourcePort"> <tr v-if="rule.sourcePort">
<td>Source Port</td> <td>Source Port</td>
<td><a-tag color="green" v-for="r in rule.sourcePort.split(',')">[[ r ]]</a-tag></td> <td><a-tag color="green" v-for="r in rule.sourcePort.split(',')">[[ r ]]</a-tag></td>
</tr> </tr>
<tr v-if="rule.vlessRoute">
<td>VLESS Route</td>
<td><a-tag color="geekblue" v-for="r in rule.vlessRoute.split(',')">[[ r ]]</a-tag></td>
</tr>
<tr v-if="rule.network"> <tr v-if="rule.network">
<td>Network</td> <td>Network</td>
<td><a-tag color="blue" v-for="r in rule.network.split(',')">[[ r ]]</a-tag></td> <td><a-tag color="blue" v-for="r in rule.network.split(',')">[[ r ]]</a-tag></td>

275
web/html/subscription.html Normal file
View File

@@ -0,0 +1,275 @@
{{ template "page/head_start" .}}
<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
<script src="{{ .base_path }}assets/moment/moment-jalali.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
{{ template "page/head_end" .}}
{{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<a-layout-content class="p-2">
<a-row type="flex" justify="center" class="mt-2">
<a-col :xs="24" :sm="22" :md="18" :lg="14" :xl="12">
<a-card hoverable class="subscription-card">
<template #title>
<a-space>
<span>{{ i18n "subscription.title" }}</span>
<a-tag>{{ .sId }}</a-tag>
</a-space>
</template>
<template #extra>
<a-popover
:overlay-class-name="themeSwitcher.currentTheme"
title='{{ i18n "menu.settings" }}'
placement="bottomRight" trigger="click">
<template #content>
<a-space direction="vertical" :size="10">
<a-theme-switch-login></a-theme-switch-login>
<span>{{ i18n "pages.settings.language"
}}</span>
<a-select ref="selectLang" class="w-100"
v-model="lang"
@change="LanguageManager.setLanguage(lang)"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value"
label="English"
v-for="l in LanguageManager.supportedLanguages"
:key="l.value">
<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>
</a-space>
</template>
<a-button shape="circle" icon="setting"></a-button>
</a-popover>
</template>
<a-form layout="vertical">
<a-form-item>
<a-space direction="vertical" align="center">
<a-row type="flex" :gutter="[8,8]"
justify="center" style="width:100%">
<a-col :xs="24" :sm="12"
style="text-align:center;">
<tr-qr-box class="qr-box">
<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 id="qrcode"
class="qr-cv"
title='{{ i18n "copy" }}'
@click="copy(app.subUrl)"></canvas>
</tr-qr-bg-inner>
</tr-qr-bg>
</tr-qr-box>
</a-col>
<a-col :xs="24" :sm="12"
style="text-align:center;">
<tr-qr-box class="qr-box">
<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 id="qrcode-subjson"
class="qr-cv"
title='{{ i18n "copy" }}'
@click="copy(app.subJsonUrl)"></canvas>
</tr-qr-bg-inner>
</tr-qr-bg>
</tr-qr-box>
</a-col>
</a-row>
</a-space>
</a-form-item>
<a-form-item>
<a-descriptions bordered :column="1" size="small">
<a-descriptions-item
label='{{ i18n "subscription.subId" }}'>[[
app.sId
]]</a-descriptions-item>
<a-descriptions-item
label='{{ i18n "subscription.status" }}'>
<template v-if="isUnlimited">
<a-tag color="purple">{{ i18n
"subscription.unlimited" }}</a-tag>
</template>
<template v-else>
<a-tag
:color="isActive ? 'green' : 'red'">[[
isActive ? '{{ i18n
"subscription.active" }}' : '{{ i18n
"subscription.inactive" }}'
]]</a-tag>
</template>
</a-descriptions-item>
<a-descriptions-item
label='{{ i18n "subscription.downloaded" }}'>[[
app.download
]]</a-descriptions-item>
<a-descriptions-item
label='{{ i18n "subscription.uploaded" }}'>[[
app.upload
]]</a-descriptions-item>
<a-descriptions-item
label='{{ i18n "usage" }}'>[[ app.used
]]</a-descriptions-item>
<a-descriptions-item
label='{{ i18n "subscription.totalQuota" }}'>[[
app.total
]]</a-descriptions-item>
<a-descriptions-item v-if="app.totalByte > 0"
label='{{ i18n "remained" }}'>[[
app.remained ]]</a-descriptions-item>
<a-descriptions-item
label='{{ i18n "lastOnline" }}'>
<template v-if="app.lastOnlineMs > 0">
<template
v-if="app.datepicker === 'gregorian'">
[[
DateUtil.formatMillis(app.lastOnlineMs)
]]
</template>
<template v-else>
[[
DateUtil.convertToJalalian(moment(app.lastOnlineMs))
]]
</template>
</template>
<template v-else>
<span>-</span>
</template>
</a-descriptions-item>
<a-descriptions-item
label='{{ i18n "subscription.expiry" }}'>
<template v-if="app.expireMs === 0">
{{ i18n "subscription.noExpiry" }}
</template>
<template v-else>
<template
v-if="app.datepicker === 'gregorian'">
[[
DateUtil.formatMillis(app.expireMs)
]]
</template>
<template v-else>
[[
DateUtil.convertToJalalian(moment(app.expireMs))
]]
</template>
</template>
</a-descriptions-item>
</a-descriptions>
</a-form-item>
</a-form>
<br />
<a-list bordered>
<a-list-item v-for="(link, idx) in links" :key="link">
<div style="width:100%; text-align:center;">
<a-button type="primary" :block="isMobile"
@click="copy(link)">[[ linkName(link, idx)
]]</a-button>
</div>
</a-list-item>
</a-list>
<br />
<a-form layout="vertical">
<a-form-item>
<a-row type="flex" justify="center" :gutter="[8,8]"
style="width:100%">
<a-col :xs="24" :sm="12"
style="text-align:center;">
<!-- Android dropdown -->
<a-dropdown :trigger="['click']">
<a-button :block="isMobile"
:style="{ marginTop: isMobile ? '6px' : 0 }"
size="large" type="primary">
Android <a-icon type="down" />
</a-button>
<a-menu slot="overlay"
:class="themeSwitcher.currentTheme">
<a-menu-item key="android-v2box"
@click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item>
<a-menu-item key="android-v2rayng"
@click="open('v2rayng://install-config?url=' + encodeURIComponent(app.subUrl))">V2RayNG</a-menu-item>
<a-menu-item key="android-singbox"
@click="copy(app.subUrl)">Sing-box</a-menu-item>
<a-menu-item key="android-v2raytun"
@click="copy(app.subUrl)">V2RayTun</a-menu-item>
<a-menu-item key="android-npvtunnel"
@click="copy(app.subUrl)">NPV
Tunnel</a-menu-item>
</a-menu>
</a-dropdown>
</a-col>
<a-col :xs="24" :sm="12"
style="text-align:center;">
<!-- iOS dropdown -->
<a-dropdown :trigger="['click']">
<a-button :block="isMobile"
:style="{ marginTop: isMobile ? '6px' : 0 }"
size="large" type="primary">
iOS <a-icon type="down" />
</a-button>
<a-menu slot="overlay"
:class="themeSwitcher.currentTheme">
<a-menu-item key="ios-shadowrocket"
@click="open('shadowrocket://add/subscription?url=' + encodeURIComponent(app.subUrl) + '&remark=' + encodeURIComponent(app.sId))">Shadowrocket</a-menu-item>
<a-menu-item key="ios-v2box"
@click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item>
<a-menu-item key="ios-streisand"
@click="open('streisand://import/' + encodeURIComponent(app.subUrl))">Streisand</a-menu-item>
<a-menu-item key="ios-v2raytun"
@click="copy(app.subUrl)">V2RayTun</a-menu-item>
<a-menu-item key="ios-npvtunnel"
@click="copy(app.subUrl)">NPV
Tunnel</a-menu-item>
</a-menu>
</a-dropdown>
</a-col>
</a-row>
</a-form-item>
</a-form>
</a-card>
</a-col>
</a-row>
</a-layout-content>
</a-layout>
<!-- Bootstrap data for external JS -->
<template id="subscription-data" data-sid="{{ .sId }}"
data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}"
data-download="{{ .download }}"
data-upload="{{ .upload }}" data-used="{{ .used }}"
data-total="{{ .total }}" data-remained="{{ .remained }}"
data-expire="{{ .expire }}" data-lastonline="{{ .lastOnline }}"
data-downloadbyte="{{ .downloadByte }}"
data-uploadbyte="{{ .uploadByte }}" data-totalbyte="{{ .totalByte }}"
data-datepicker="{{ .datepicker }}"></template>
<textarea id="subscription-links"
style="display:none">{{ range .result }}{{ . }}
{{ end }}</textarea>
{{template "component/aThemeSwitch" .}}
<script src="{{ .base_path }}assets/js/subscription.js?{{ .cur_ver }}"></script>
{{ template "page/body_end" .}}

View File

@@ -3,45 +3,10 @@
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
<style>
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
@media (max-width: 768px) {
.ant-tabs-nav .ant-tabs-tab {
margin: 0;
padding: 12px .5rem;
}
.ant-table-thead>tr>th,
.ant-table-tbody>tr>td {
padding: 10px 0px;
}
}
.ant-tabs-bar {
margin: 0;
}
.ant-list-item {
display: block;
}
.ant-list-item>li {
padding: 10px 20px !important;
}
.ant-collapse-content-box .ant-alert {
margin-block-end: 12px;
}
</style>
{{ template "page/head_end" .}} {{ template "page/head_end" .}}
{{ template "page/body_start" .}} {{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme"> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' xray-page'">
<a-sidebar></a-sidebar> <a-sidebar></a-sidebar>
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content> <a-layout-content>
@@ -181,8 +146,9 @@
{ title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } }, { title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } },
{ {
title: '{{ i18n "pages.xray.rules.source"}}', children: [ title: '{{ i18n "pages.xray.rules.source"}}', children: [
{ title: 'IP', dataIndex: "source", align: 'center', width: 20, ellipsis: true }, { title: 'IP', dataIndex: "sourceIP", align: 'center', width: 20, ellipsis: true },
{ title: '{{ i18n "pages.inbounds.port" }}', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true }] { title: '{{ i18n "pages.inbounds.port" }}', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true },
{ title: 'VLESS Route', dataIndex: 'vlessRoute', align: 'center', width: 15, ellipsis: true }]
}, },
{ {
title: '{{ i18n "pages.inbounds.network"}}', children: [ title: '{{ i18n "pages.inbounds.network"}}', children: [
@@ -351,7 +317,7 @@
{ label: '🇨🇳 China', value: 'geosite:cn' }, { label: '🇨🇳 China', value: 'geosite:cn' },
{ label: '🇨🇳 .cn', value: 'regexp:.*\\.cn$' }, { label: '🇨🇳 .cn', value: 'regexp:.*\\.cn$' },
{ label: '🇷🇺 Russia', value: 'ext:geosite_RU.dat:ru-available-only-inside' }, { label: '🇷🇺 Russia', value: 'ext:geosite_RU.dat:ru-available-only-inside' },
{ label: '🇷🇺 .ru', value: 'regexp:.*\\.ru' }, { label: '🇷🇺 .ru', value: 'regexp:.*\\.ru$' },
{ label: '🇷🇺 .su', value: 'regexp:.*\\.su$' }, { label: '🇷🇺 .su', value: 'regexp:.*\\.su$' },
{ label: '🇷🇺 .рф', value: 'regexp:.*\\.xn--p1ai$' }, { label: '🇷🇺 .рф', value: 'regexp:.*\\.xn--p1ai$' },
{ label: '🇻🇳 .vn', value: 'regexp:.*\\.vn$' }, { label: '🇻🇳 .vn', value: 'regexp:.*\\.vn$' },
@@ -420,7 +386,7 @@
}, },
async restartXray() { async restartXray() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post("server/restartXrayService"); const msg = await HttpUtil.post("/panel/api/server/restartXrayService");
this.loading(false); this.loading(false);
if (msg.success) { if (msg.success) {
await PromiseUtil.sleep(500); await PromiseUtil.sleep(500);
@@ -572,7 +538,7 @@
serverObj = o.settings.vnext; serverObj = o.settings.vnext;
break; break;
case Protocols.HTTP: case Protocols.HTTP:
case Protocols.Socks: case Protocols.Mixed:
case Protocols.Shadowsocks: case Protocols.Shadowsocks:
case Protocols.Trojan: case Protocols.Trojan:
serverObj = o.settings.servers; serverObj = o.settings.servers;

View File

@@ -8,10 +8,10 @@ import (
"os" "os"
"os/exec" "os/exec"
"regexp" "regexp"
"runtime"
"sort" "sort"
"time" "time"
"slices"
"x-ui/database" "x-ui/database"
"x-ui/database/model" "x-ui/database/model"
"x-ui/logger" "x-ui/logger"
@@ -40,12 +40,20 @@ func (j *CheckClientIpJob) Run() {
f2bInstalled := j.checkFail2BanInstalled() f2bInstalled := j.checkFail2BanInstalled()
isAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive) isAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive)
if iplimitActive { if isAccessLogAvailable {
if f2bInstalled && isAccessLogAvailable { if runtime.GOOS == "windows" {
shouldClearAccessLog = j.processLogFile() if iplimitActive {
shouldClearAccessLog = j.processLogFile()
}
} else { } else {
if !f2bInstalled { if iplimitActive {
logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.") if f2bInstalled {
shouldClearAccessLog = j.processLogFile()
} else {
if !f2bInstalled {
logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.")
}
}
} }
} }
} }
@@ -58,21 +66,21 @@ func (j *CheckClientIpJob) Run() {
func (j *CheckClientIpJob) clearAccessLog() { func (j *CheckClientIpJob) clearAccessLog() {
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
j.checkError(err) j.checkError(err)
defer logAccessP.Close()
accessLogPath, err := xray.GetAccessLogPath() accessLogPath, err := xray.GetAccessLogPath()
j.checkError(err) j.checkError(err)
file, err := os.Open(accessLogPath) file, err := os.Open(accessLogPath)
j.checkError(err) j.checkError(err)
defer file.Close()
_, err = io.Copy(logAccessP, file) _, err = io.Copy(logAccessP, file)
j.checkError(err) j.checkError(err)
logAccessP.Close()
file.Close()
err = os.Truncate(accessLogPath, 0) err = os.Truncate(accessLogPath, 0)
j.checkError(err) j.checkError(err)
j.lastClear = time.Now().Unix() j.lastClear = time.Now().Unix()
} }
@@ -193,10 +201,6 @@ func (j *CheckClientIpJob) checkError(e error) {
} }
} }
func (j *CheckClientIpJob) contains(s []string, str string) bool {
return slices.Contains(s, str)
}
func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) { func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
db := database.GetDB() db := database.GetDB()
InboundClientIps := &model.InboundClientIps{} InboundClientIps := &model.InboundClientIps{}

View File

@@ -16,7 +16,7 @@ func NewCheckXrayRunningJob() *CheckXrayRunningJob {
} }
func (j *CheckXrayRunningJob) Run() { func (j *CheckXrayRunningJob) Run() {
if j.xrayService.IsXrayRunning() { if !j.xrayService.DidXrayCrash() {
j.checkTime = 0 j.checkTime = 0
} else { } else {
j.checkTime++ j.checkTime++

View File

@@ -3,6 +3,7 @@ package locale
import ( import (
"embed" "embed"
"io/fs" "io/fs"
"os"
"strings" "strings"
"x-ui/logger" "x-ui/logger"
@@ -78,6 +79,11 @@ func I18n(i18nType I18nType, key string, params ...string) string {
templateData := createTemplateData(params) templateData := createTemplateData(params)
if localizer == nil {
// Fallback to key if localizer not ready; prevents nil panic on pages like sub
return key
}
msg, err := localizer.Localize(&i18n.LocalizeConfig{ msg, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: key, MessageID: key,
TemplateData: templateData, TemplateData: templateData,
@@ -102,6 +108,15 @@ func initTGBotLocalizer(settingService SettingService) error {
func LocalizerMiddleware() gin.HandlerFunc { func LocalizerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// Ensure bundle is initialized so creating a Localizer won't panic
if i18nBundle == nil {
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
// Try lazy-load from disk when running sub server without InitLocalizer
if err := loadTranslationsFromDisk(i18nBundle); err != nil {
logger.Warning("i18n lazy load failed:", err)
}
}
var lang string var lang string
if cookie, err := c.Request.Cookie("lang"); err == nil { if cookie, err := c.Request.Cookie("lang"); err == nil {
@@ -118,6 +133,25 @@ func LocalizerMiddleware() gin.HandlerFunc {
} }
} }
// loadTranslationsFromDisk attempts to load translation files from "web/translation" using the local filesystem.
func loadTranslationsFromDisk(bundle *i18n.Bundle) error {
root := os.DirFS("web")
return fs.WalkDir(root, "translation", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
data, err := fs.ReadFile(root, path)
if err != nil {
return err
}
_, err = bundle.ParseMessageFileBytes(data, path)
return err
})
}
func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error { func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
err := fs.WalkDir(i18nFS, "translation", err := fs.WalkDir(i18nFS, "translation",
func(path string, d fs.DirEntry, err error) error { func(path string, d fs.DirEntry, err error) error {

View File

@@ -19,7 +19,7 @@
"tag": "api", "tag": "api",
"listen": "127.0.0.1", "listen": "127.0.0.1",
"port": 62789, "port": 62789,
"protocol": "dokodemo-door", "protocol": "tunnel",
"settings": { "settings": {
"address": "127.0.0.1" "address": "127.0.0.1"
} }

View File

@@ -175,17 +175,42 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
return inbound, false, err return inbound, false, err
} }
// Ensure created_at and updated_at on clients in settings
if len(clients) > 0 {
var settings map[string]any
if err2 := json.Unmarshal([]byte(inbound.Settings), &settings); err2 == nil && settings != nil {
now := time.Now().Unix() * 1000
updatedClients := make([]model.Client, 0, len(clients))
for _, c := range clients {
if c.CreatedAt == 0 {
c.CreatedAt = now
}
c.UpdatedAt = now
updatedClients = append(updatedClients, c)
}
settings["clients"] = updatedClients
if bs, err3 := json.MarshalIndent(settings, "", " "); err3 == nil {
inbound.Settings = string(bs)
} else {
logger.Debug("Unable to marshal inbound settings with timestamps:", err3)
}
} else if err2 != nil {
logger.Debug("Unable to parse inbound settings for timestamps:", err2)
}
}
// Secure client ID // Secure client ID
for _, client := range clients { for _, client := range clients {
if inbound.Protocol == "trojan" { switch inbound.Protocol {
case "trojan":
if client.Password == "" { if client.Password == "" {
return inbound, false, common.NewError("empty client ID") return inbound, false, common.NewError("empty client ID")
} }
} else if inbound.Protocol == "shadowsocks" { case "shadowsocks":
if client.Email == "" { if client.Email == "" {
return inbound, false, common.NewError("empty client ID") return inbound, false, common.NewError("empty client ID")
} }
} else { default:
if client.ID == "" { if client.ID == "" {
return inbound, false, common.NewError("empty client ID") return inbound, false, common.NewError("empty client ID")
} }
@@ -319,6 +344,65 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
return inbound, false, err return inbound, false, err
} }
// Ensure created_at and updated_at exist in inbound.Settings clients
{
var oldSettings map[string]any
_ = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
emailToCreated := map[string]int64{}
emailToUpdated := map[string]int64{}
if oldSettings != nil {
if oc, ok := oldSettings["clients"].([]any); ok {
for _, it := range oc {
if m, ok2 := it.(map[string]any); ok2 {
if email, ok3 := m["email"].(string); ok3 {
switch v := m["created_at"].(type) {
case float64:
emailToCreated[email] = int64(v)
case int64:
emailToCreated[email] = v
}
switch v := m["updated_at"].(type) {
case float64:
emailToUpdated[email] = int64(v)
case int64:
emailToUpdated[email] = v
}
}
}
}
}
}
var newSettings map[string]any
if err2 := json.Unmarshal([]byte(inbound.Settings), &newSettings); err2 == nil && newSettings != nil {
now := time.Now().Unix() * 1000
if nSlice, ok := newSettings["clients"].([]any); ok {
for i := range nSlice {
if m, ok2 := nSlice[i].(map[string]any); ok2 {
email, _ := m["email"].(string)
if _, ok3 := m["created_at"]; !ok3 {
if v, ok4 := emailToCreated[email]; ok4 && v > 0 {
m["created_at"] = v
} else {
m["created_at"] = now
}
}
// Preserve client's updated_at if present; do not bump on parent inbound update
if _, hasUpdated := m["updated_at"]; !hasUpdated {
if v, ok4 := emailToUpdated[email]; ok4 && v > 0 {
m["updated_at"] = v
}
}
nSlice[i] = m
}
}
newSettings["clients"] = nSlice
if bs, err3 := json.MarshalIndent(newSettings, "", " "); err3 == nil {
inbound.Settings = string(bs)
}
}
}
}
oldInbound.Up = inbound.Up oldInbound.Up = inbound.Up
oldInbound.Down = inbound.Down oldInbound.Down = inbound.Down
oldInbound.Total = inbound.Total oldInbound.Total = inbound.Total
@@ -331,7 +415,6 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
oldInbound.Settings = inbound.Settings oldInbound.Settings = inbound.Settings
oldInbound.StreamSettings = inbound.StreamSettings oldInbound.StreamSettings = inbound.StreamSettings
oldInbound.Sniffing = inbound.Sniffing oldInbound.Sniffing = inbound.Sniffing
oldInbound.Allocate = inbound.Allocate
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
} else { } else {
@@ -421,6 +504,17 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
} }
interfaceClients := settings["clients"].([]any) interfaceClients := settings["clients"].([]any)
// Add timestamps for new clients being appended
nowTs := time.Now().Unix() * 1000
for i := range interfaceClients {
if cm, ok := interfaceClients[i].(map[string]any); ok {
if _, ok2 := cm["created_at"]; !ok2 {
cm["created_at"] = nowTs
}
cm["updated_at"] = nowTs
interfaceClients[i] = cm
}
}
existEmail, err := s.checkEmailsExistForClients(clients) existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil { if err != nil {
return false, err return false, err
@@ -436,15 +530,16 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
// Secure client ID // Secure client ID
for _, client := range clients { for _, client := range clients {
if oldInbound.Protocol == "trojan" { switch oldInbound.Protocol {
case "trojan":
if client.Password == "" { if client.Password == "" {
return false, common.NewError("empty client ID") return false, common.NewError("empty client ID")
} }
} else if oldInbound.Protocol == "shadowsocks" { case "shadowsocks":
if client.Email == "" { if client.Email == "" {
return false, common.NewError("empty client ID") return false, common.NewError("empty client ID")
} }
} else { default:
if client.ID == "" { if client.ID == "" {
return false, common.NewError("empty client ID") return false, common.NewError("empty client ID")
} }
@@ -631,13 +726,14 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
clientIndex := -1 clientIndex := -1
for index, oldClient := range oldClients { for index, oldClient := range oldClients {
oldClientId := "" oldClientId := ""
if oldInbound.Protocol == "trojan" { switch oldInbound.Protocol {
case "trojan":
oldClientId = oldClient.Password oldClientId = oldClient.Password
newClientId = clients[0].Password newClientId = clients[0].Password
} else if oldInbound.Protocol == "shadowsocks" { case "shadowsocks":
oldClientId = oldClient.Email oldClientId = oldClient.Email
newClientId = clients[0].Email newClientId = clients[0].Email
} else { default:
oldClientId = oldClient.ID oldClientId = oldClient.ID
newClientId = clients[0].ID newClientId = clients[0].ID
} }
@@ -669,6 +765,25 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
return false, err return false, err
} }
settingsClients := oldSettings["clients"].([]any) settingsClients := oldSettings["clients"].([]any)
// Preserve created_at and set updated_at for the replacing client
var preservedCreated any
if clientIndex >= 0 && clientIndex < len(settingsClients) {
if oldMap, ok := settingsClients[clientIndex].(map[string]any); ok {
if v, ok2 := oldMap["created_at"]; ok2 {
preservedCreated = v
}
}
}
if len(interfaceClients) > 0 {
if newMap, ok := interfaceClients[0].(map[string]any); ok {
if preservedCreated == nil {
preservedCreated = time.Now().Unix() * 1000
}
newMap["created_at"] = preservedCreated
newMap["updated_at"] = time.Now().Unix() * 1000
interfaceClients[0] = newMap
}
}
settingsClients[clientIndex] = interfaceClients[0] settingsClients[clientIndex] = interfaceClients[0]
oldSettings["clients"] = settingsClients oldSettings["clients"] = settingsClients
@@ -811,8 +926,9 @@ func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic
if traffic.IsInbound { if traffic.IsInbound {
err = tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag). err = tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
Updates(map[string]any{ Updates(map[string]any{
"up": gorm.Expr("up + ?", traffic.Up), "up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down), "down": gorm.Expr("down + ?", traffic.Down),
"all_time": gorm.Expr("COALESCE(all_time, 0) + ?", traffic.Up+traffic.Down),
}).Error }).Error
if err != nil { if err != nil {
return err return err
@@ -858,10 +974,12 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email { if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email {
dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up
dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down
dbClientTraffics[dbTraffic_index].AllTime += (traffics[traffic_index].Up + traffics[traffic_index].Down)
// Add user in onlineUsers array on traffic // Add user in onlineUsers array on traffic
if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 { if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
onlineClients = append(onlineClients, traffics[traffic_index].Email) onlineClients = append(onlineClients, traffics[traffic_index].Email)
dbClientTraffics[dbTraffic_index].LastOnline = time.Now().UnixMilli()
} }
break break
} }
@@ -906,10 +1024,16 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
oldExpiryTime := c["expiryTime"].(float64) oldExpiryTime := c["expiryTime"].(float64)
newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime) newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime)
c["expiryTime"] = newExpiryTime c["expiryTime"] = newExpiryTime
c["updated_at"] = time.Now().Unix() * 1000
dbClientTraffics[traffic_index].ExpiryTime = newExpiryTime dbClientTraffics[traffic_index].ExpiryTime = newExpiryTime
break break
} }
} }
// Backfill created_at and updated_at
if _, ok := c["created_at"]; !ok {
c["created_at"] = time.Now().Unix() * 1000
}
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c)) newClients = append(newClients, any(c))
} }
settings["clients"] = newClients settings["clients"] = newClients
@@ -1244,11 +1368,12 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (boo
for _, oldClient := range oldClients { for _, oldClient := range oldClients {
if oldClient.Email == clientEmail { if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" { switch inbound.Protocol {
case "trojan":
clientId = oldClient.Password clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" { case "shadowsocks":
clientId = oldClient.Email clientId = oldClient.Email
} else { default:
clientId = oldClient.ID clientId = oldClient.ID
} }
break break
@@ -1270,6 +1395,7 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (boo
c := clients[client_index].(map[string]any) c := clients[client_index].(map[string]any)
if c["email"] == clientEmail { if c["email"] == clientEmail {
c["tgId"] = tgId c["tgId"] = tgId
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c)) newClients = append(newClients, any(c))
} }
} }
@@ -1328,11 +1454,12 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
for _, oldClient := range oldClients { for _, oldClient := range oldClients {
if oldClient.Email == clientEmail { if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" { switch inbound.Protocol {
case "trojan":
clientId = oldClient.Password clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" { case "shadowsocks":
clientId = oldClient.Email clientId = oldClient.Email
} else { default:
clientId = oldClient.ID clientId = oldClient.ID
} }
clientOldEnabled = oldClient.Enable clientOldEnabled = oldClient.Enable
@@ -1355,6 +1482,7 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
c := clients[client_index].(map[string]any) c := clients[client_index].(map[string]any)
if c["email"] == clientEmail { if c["email"] == clientEmail {
c["enable"] = !clientOldEnabled c["enable"] = !clientOldEnabled
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c)) newClients = append(newClients, any(c))
} }
} }
@@ -1391,11 +1519,12 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
for _, oldClient := range oldClients { for _, oldClient := range oldClients {
if oldClient.Email == clientEmail { if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" { switch inbound.Protocol {
case "trojan":
clientId = oldClient.Password clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" { case "shadowsocks":
clientId = oldClient.Email clientId = oldClient.Email
} else { default:
clientId = oldClient.ID clientId = oldClient.ID
} }
break break
@@ -1417,6 +1546,7 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
c := clients[client_index].(map[string]any) c := clients[client_index].(map[string]any)
if c["email"] == clientEmail { if c["email"] == clientEmail {
c["limitIp"] = count c["limitIp"] = count
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c)) newClients = append(newClients, any(c))
} }
} }
@@ -1448,11 +1578,12 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
for _, oldClient := range oldClients { for _, oldClient := range oldClients {
if oldClient.Email == clientEmail { if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" { switch inbound.Protocol {
case "trojan":
clientId = oldClient.Password clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" { case "shadowsocks":
clientId = oldClient.Email clientId = oldClient.Email
} else { default:
clientId = oldClient.ID clientId = oldClient.ID
} }
break break
@@ -1474,6 +1605,7 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
c := clients[client_index].(map[string]any) c := clients[client_index].(map[string]any)
if c["email"] == clientEmail { if c["email"] == clientEmail {
c["expiryTime"] = expiry_time c["expiryTime"] = expiry_time
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c)) newClients = append(newClients, any(c))
} }
} }
@@ -1508,11 +1640,12 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
for _, oldClient := range oldClients { for _, oldClient := range oldClients {
if oldClient.Email == clientEmail { if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" { switch inbound.Protocol {
case "trojan":
clientId = oldClient.Password clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" { case "shadowsocks":
clientId = oldClient.Email clientId = oldClient.Email
} else { default:
clientId = oldClient.ID clientId = oldClient.ID
} }
break break
@@ -1534,6 +1667,7 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
c := clients[client_index].(map[string]any) c := clients[client_index].(map[string]any)
if c["email"] == clientEmail { if c["email"] == clientEmail {
c["totalGB"] = totalGB * 1024 * 1024 * 1024 c["totalGB"] = totalGB * 1024 * 1024 * 1024
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c)) newClients = append(newClients, any(c))
} }
} }
@@ -1785,6 +1919,21 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
return nil, nil return nil, nil
} }
func (s *InboundService) UpdateClientTrafficByEmail(email string, upload int64, download int64) error {
db := database.GetDB()
result := db.Model(xray.ClientTraffic{}).
Where("email = ?", email).
Updates(map[string]any{"up": upload, "down": download})
err := result.Error
if err != nil {
logger.Warningf("Error updating ClientTraffic with email %s: %v", email, err)
return err
}
return nil
}
func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic, error) { func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic, error) {
db := database.GetDB() db := database.GetDB()
var traffics []xray.ClientTraffic var traffics []xray.ClientTraffic
@@ -1901,6 +2050,25 @@ func (s *InboundService) MigrationRequirements() {
} }
}() }()
// Calculate and backfill all_time from up+down for inbounds and clients
err = tx.Exec(`
UPDATE inbounds
SET all_time = IFNULL(up, 0) + IFNULL(down, 0)
WHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0
`).Error
if err != nil {
return
}
err = tx.Exec(`
UPDATE client_traffics
SET all_time = IFNULL(up, 0) + IFNULL(down, 0)
WHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0
`).Error
if err != nil {
return
}
// Fix inbounds based problems // Fix inbounds based problems
var inbounds []*model.Inbound var inbounds []*model.Inbound
err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
@@ -1939,6 +2107,11 @@ func (s *InboundService) MigrationRequirements() {
c["flow"] = "" c["flow"] = ""
} }
} }
// Backfill created_at and updated_at
if _, ok := c["created_at"]; !ok {
c["created_at"] = time.Now().Unix() * 1000
}
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c)) newClients = append(newClients, any(c))
} }
settings["clients"] = newClients settings["clients"] = newClients
@@ -2027,6 +2200,20 @@ func (s *InboundService) GetOnlineClients() []string {
return p.GetOnlineClients() return p.GetOnlineClients()
} }
func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) {
db := database.GetDB()
var rows []xray.ClientTraffic
err := db.Model(&xray.ClientTraffic{}).Select("email, last_online").Find(&rows).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
result := make(map[string]int64, len(rows))
for _, r := range rows {
result[r.Email] = r.LastOnline
}
return result, nil
}
func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) { func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) {
db := database.GetDB() db := database.GetDB()
@@ -2060,3 +2247,95 @@ func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, [
return validEmails, extraEmails, nil return validEmails, extraEmails, nil
} }
func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (bool, error) {
oldInbound, err := s.GetInbound(inboundId)
if err != nil {
logger.Error("Load Old Data Error")
return false, err
}
var settings map[string]any
if err := json.Unmarshal([]byte(oldInbound.Settings), &settings); err != nil {
return false, err
}
interfaceClients, ok := settings["clients"].([]any)
if !ok {
return false, common.NewError("invalid clients format in inbound settings")
}
var newClients []any
needApiDel := false
found := false
for _, client := range interfaceClients {
c, ok := client.(map[string]any)
if !ok {
continue
}
if cEmail, ok := c["email"].(string); ok && cEmail == email {
// matched client, drop it
found = true
needApiDel, _ = c["enable"].(bool)
} else {
newClients = append(newClients, client)
}
}
if !found {
return false, common.NewError(fmt.Sprintf("client with email %s not found", email))
}
if len(newClients) == 0 {
return false, common.NewError("no client remained in Inbound")
}
settings["clients"] = newClients
newSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return false, err
}
oldInbound.Settings = string(newSettings)
db := database.GetDB()
// remove IP bindings
if err := s.DelClientIPs(db, email); err != nil {
logger.Error("Error in delete client IPs")
return false, err
}
needRestart := false
// remove stats too
if len(email) > 0 {
traffic, err := s.GetClientTrafficByEmail(email)
if err != nil {
return false, err
}
if traffic != nil {
if err := s.DelClientStat(db, email); err != nil {
logger.Error("Delete stats Data Error")
return false, err
}
}
if needApiDel {
s.xrayApi.Init(p.GetAPIPort())
if err1 := s.xrayApi.RemoveUser(oldInbound.Tag, email); err1 == nil {
logger.Debug("Client deleted by api:", email)
needRestart = false
} else {
if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", email)) {
logger.Debug("User is already deleted. Nothing to do more...")
} else {
logger.Debug("Error in deleting client by api:", err1)
needRestart = true
}
}
s.xrayApi.Close()
}
}
return needRestart, db.Save(oldInbound).Error
}

View File

@@ -2,6 +2,7 @@ package service
import ( import (
"archive/zip" "archive/zip"
"bufio"
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -11,6 +12,7 @@ import (
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@@ -23,6 +25,7 @@ import (
"x-ui/util/sys" "x-ui/util/sys"
"x-ui/xray" "x-ui/xray"
"github.com/google/uuid"
"github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/disk" "github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/host" "github.com/shirou/gopsutil/v4/host"
@@ -94,21 +97,34 @@ type ServerService struct {
inboundService InboundService inboundService InboundService
cachedIPv4 string cachedIPv4 string
cachedIPv6 string cachedIPv6 string
noIPv6 bool
} }
func getPublicIP(url string) string { func getPublicIP(url string) string {
resp, err := http.Get(url) client := &http.Client{
Timeout: 3 * time.Second,
}
resp, err := client.Get(url)
if err != nil { if err != nil {
return "N/A" return "N/A"
} }
defer resp.Body.Close() defer resp.Body.Close()
// Don't retry if access is blocked or region-restricted
if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnavailableForLegalReasons {
return "N/A"
}
if resp.StatusCode != http.StatusOK {
return "N/A"
}
ip, err := io.ReadAll(resp.Body) ip, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "N/A" return "N/A"
} }
ipString := string(ip) ipString := strings.TrimSpace(string(ip))
if ipString == "" { if ipString == "" {
return "N/A" return "N/A"
} }
@@ -221,10 +237,44 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
} }
// IP fetching with caching // IP fetching with caching
if s.cachedIPv4 == "" || s.cachedIPv6 == "" { showIp4ServiceLists := []string{
s.cachedIPv4 = getPublicIP("https://api.ipify.org") "https://api4.ipify.org",
s.cachedIPv6 = getPublicIP("https://api6.ipify.org") "https://ipv4.icanhazip.com",
"https://v4.api.ipinfo.io/ip",
"https://ipv4.myexternalip.com/raw",
"https://4.ident.me",
"https://check-host.net/ip",
} }
showIp6ServiceLists := []string{
"https://api6.ipify.org",
"https://ipv6.icanhazip.com",
"https://v6.api.ipinfo.io/ip",
"https://ipv6.myexternalip.com/raw",
"https://6.ident.me",
}
if s.cachedIPv4 == "" {
for _, ip4Service := range showIp4ServiceLists {
s.cachedIPv4 = getPublicIP(ip4Service)
if s.cachedIPv4 != "N/A" {
break
}
}
}
if s.cachedIPv6 == "" && !s.noIPv6 {
for _, ip6Service := range showIp6ServiceLists {
s.cachedIPv6 = getPublicIP(ip6Service)
if s.cachedIPv6 != "N/A" {
break
}
}
}
if s.cachedIPv6 == "N/A" {
s.noIPv6 = true
}
status.PublicIP.IPv4 = s.cachedIPv4 status.PublicIP.IPv4 = s.cachedIPv4
status.PublicIP.IPv6 = s.cachedIPv6 status.PublicIP.IPv6 = s.cachedIPv6
@@ -295,7 +345,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
continue continue
} }
if major > 25 || (major == 25 && minor > 6) || (major == 25 && minor == 6 && patch >= 8) { if major > 25 || (major == 25 && minor > 9) || (major == 25 && minor == 9 && patch >= 11) {
versions = append(versions, release.TagName) versions = append(versions, release.TagName)
} }
} }
@@ -312,7 +362,6 @@ func (s *ServerService) StopXrayService() error {
} }
func (s *ServerService) RestartXrayService() error { func (s *ServerService) RestartXrayService() error {
s.xrayService.StopXray()
err := s.xrayService.RestartXray(true) err := s.xrayService.RestartXray(true)
if err != nil { if err != nil {
logger.Error("start xray failed:", err) logger.Error("start xray failed:", err)
@@ -328,6 +377,8 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
switch osName { switch osName {
case "darwin": case "darwin":
osName = "macos" osName = "macos"
case "windows":
osName = "windows"
} }
switch arch { switch arch {
@@ -371,19 +422,23 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
} }
func (s *ServerService) UpdateXray(version string) error { func (s *ServerService) UpdateXray(version string) error {
// 1. Stop xray before doing anything
if err := s.StopXrayService(); err != nil {
logger.Warning("failed to stop xray before update:", err)
}
// 2. Download the zip
zipFileName, err := s.downloadXRay(version) zipFileName, err := s.downloadXRay(version)
if err != nil { if err != nil {
return err return err
} }
defer os.Remove(zipFileName)
zipFile, err := os.Open(zipFileName) zipFile, err := os.Open(zipFileName)
if err != nil { if err != nil {
return err return err
} }
defer func() { defer zipFile.Close()
zipFile.Close()
os.Remove(zipFileName)
}()
stat, err := zipFile.Stat() stat, err := zipFile.Stat()
if err != nil { if err != nil {
@@ -394,19 +449,14 @@ func (s *ServerService) UpdateXray(version string) error {
return err return err
} }
s.xrayService.StopXray() // 3. Helper to extract files
defer func() {
err := s.xrayService.RestartXray(true)
if err != nil {
logger.Error("start xray failed:", err)
}
}()
copyZipFile := func(zipName string, fileName string) error { copyZipFile := func(zipName string, fileName string) error {
zipFile, err := reader.Open(zipName) zipFile, err := reader.Open(zipName)
if err != nil { if err != nil {
return err return err
} }
defer zipFile.Close()
os.MkdirAll(filepath.Dir(fileName), 0755)
os.Remove(fileName) os.Remove(fileName)
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, fs.ModePerm) file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, fs.ModePerm)
if err != nil { if err != nil {
@@ -417,11 +467,23 @@ func (s *ServerService) UpdateXray(version string) error {
return err return err
} }
err = copyZipFile("xray", xray.GetBinaryPath()) // 4. Extract correct binary
if runtime.GOOS == "windows" {
targetBinary := filepath.Join("bin", "xray-windows-amd64.exe")
err = copyZipFile("xray.exe", targetBinary)
} else {
err = copyZipFile("xray", xray.GetBinaryPath())
}
if err != nil { if err != nil {
return err return err
} }
// 5. Restart xray
if err := s.xrayService.RestartXray(true); err != nil {
logger.Error("start xray failed:", err)
return err
}
return nil return nil
} }
@@ -447,6 +509,81 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str
return lines return lines
} }
func (s *ServerService) GetXrayLogs(
count string,
filter string,
showDirect string,
showBlocked string,
showProxy string,
freedoms []string,
blackholes []string) []string {
countInt, _ := strconv.Atoi(count)
var lines []string
pathToAccessLog, err := xray.GetAccessLogPath()
if err != nil {
return lines
}
file, err := os.Open(pathToAccessLog)
if err != nil {
return lines
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.Contains(line, "api -> api") {
//skipping empty lines and api calls
continue
}
if filter != "" && !strings.Contains(line, filter) {
//applying filter if it's not empty
continue
}
//adding suffixes to further distinguish entries by outbound
if hasSuffix(line, freedoms) {
if showDirect == "false" {
continue
}
line = line + " f"
} else if hasSuffix(line, blackholes) {
if showBlocked == "false" {
continue
}
line = line + " b"
} else {
if showProxy == "false" {
continue
}
line = line + " p"
}
lines = append(lines, line)
}
if len(lines) > countInt {
lines = lines[len(lines)-countInt:]
}
return lines
}
func hasSuffix(line string, suffixes []string) bool {
for _, sfx := range suffixes {
if strings.HasSuffix(line, sfx+"]") {
return true
}
}
return false
}
func (s *ServerService) GetConfigJson() (any, error) { func (s *ServerService) GetConfigJson() (any, error) {
config, err := s.xrayService.GetXrayConfig() config, err := s.xrayService.GetXrayConfig()
if err != nil { if err != nil {
@@ -632,27 +769,43 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
return nil return nil
} }
var fileURL string var errorMessages []string
for _, file := range files {
if file.FileName == fileName { if fileName == "" {
fileURL = file.URL for _, file := range files {
break destPath := fmt.Sprintf("%s/%s", config.GetBinFolderPath(), file.FileName)
if err := downloadFile(file.URL, destPath); err != nil {
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", file.FileName, err))
}
} }
} } else {
destPath := fmt.Sprintf("%s/%s", config.GetBinFolderPath(), fileName)
if fileURL == "" { var fileURL string
return common.NewErrorf("File '%s' not found in the list of Geofiles", fileName) for _, file := range files {
} if file.FileName == fileName {
fileURL = file.URL
break
}
}
destPath := fmt.Sprintf("%s/%s", config.GetBinFolderPath(), fileName) if fileURL == "" {
errorMessages = append(errorMessages, fmt.Sprintf("File '%s' not found in the list of Geofiles", fileName))
}
if err := downloadFile(fileURL, destPath); err != nil { if err := downloadFile(fileURL, destPath); err != nil {
return common.NewErrorf("Error downloading Geofile '%s': %v", fileName, err) errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", fileName, err))
}
} }
err := s.RestartXrayService() err := s.RestartXrayService()
if err != nil { if err != nil {
return common.NewErrorf("Updated Geofile '%s' but Failed to start Xray: %v", fileName, err) errorMessages = append(errorMessages, fmt.Sprintf("Updated Geofile '%s' but Failed to start Xray: %v", fileName, err))
}
if len(errorMessages) > 0 {
return common.NewErrorf("%s", strings.Join(errorMessages, "\r\n"))
} }
return nil return nil
@@ -683,3 +836,130 @@ func (s *ServerService) GetNewX25519Cert() (any, error) {
return keyPair, nil return keyPair, nil
} }
func (s *ServerService) GetNewmldsa65() (any, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "mldsa65")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
SeedLine := strings.Split(lines[0], ":")
VerifyLine := strings.Split(lines[1], ":")
seed := strings.TrimSpace(SeedLine[1])
verify := strings.TrimSpace(VerifyLine[1])
keyPair := map[string]any{
"seed": seed,
"verify": verify,
}
return keyPair, nil
}
func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
if len(lines) < 4 {
return nil, common.NewError("invalid ech cert")
}
configList := lines[1]
serverKeys := lines[3]
return map[string]interface{}{
"echServerKeys": serverKeys,
"echConfigList": configList,
}, nil
}
func (s *ServerService) GetNewVlessEnc() (any, error) {
cmd := exec.Command(xray.GetBinaryPath(), "vlessenc")
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
var auths []map[string]string
var current map[string]string
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "Authentication:") {
if current != nil {
auths = append(auths, current)
}
current = map[string]string{
"label": strings.TrimSpace(strings.TrimPrefix(line, "Authentication:")),
}
} else if strings.HasPrefix(line, `"decryption"`) || strings.HasPrefix(line, `"encryption"`) {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 && current != nil {
key := strings.Trim(parts[0], `" `)
val := strings.Trim(parts[1], `" `)
current[key] = val
}
}
}
if current != nil {
auths = append(auths, current)
}
return map[string]any{
"auths": auths,
}, nil
}
func (s *ServerService) GetNewUUID() (map[string]string, error) {
newUUID, err := uuid.NewRandom()
if err != nil {
return nil, fmt.Errorf("failed to generate UUID: %w", err)
}
return map[string]string{
"uuid": newUUID.String(),
}, nil
}
func (s *ServerService) GetNewmlkem768() (any, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "mlkem768")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
SeedLine := strings.Split(lines[0], ":")
ClientLine := strings.Split(lines[1], ":")
seed := strings.TrimSpace(SeedLine[1])
client := strings.TrimSpace(ClientLine[1])
keyPair := map[string]any{
"seed": seed,
"client": client,
}
return keyPair, nil
}

View File

@@ -32,7 +32,7 @@ var defaultValueMap = map[string]string{
"webKeyFile": "", "webKeyFile": "",
"secret": random.Seq(32), "secret": random.Seq(32),
"webBasePath": "/", "webBasePath": "/",
"sessionMaxAge": "60", "sessionMaxAge": "360",
"pageSize": "50", "pageSize": "50",
"expireDiff": "0", "expireDiff": "0",
"trafficDiff": "0", "trafficDiff": "0",

View File

@@ -1,13 +1,16 @@
package service package service
import ( import (
"context"
"crypto/rand" "crypto/rand"
"embed" "embed"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io"
"math/big" "math/big"
"net" "net"
"net/http"
"net/url" "net/url"
"os" "os"
"regexp" "regexp"
@@ -28,6 +31,7 @@ import (
"github.com/mymmrac/telego" "github.com/mymmrac/telego"
th "github.com/mymmrac/telego/telegohandler" th "github.com/mymmrac/telego/telegohandler"
tu "github.com/mymmrac/telego/telegoutil" tu "github.com/mymmrac/telego/telegoutil"
"github.com/skip2/go-qrcode"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttpproxy" "github.com/valyala/fasthttp/fasthttpproxy"
) )
@@ -39,7 +43,6 @@ var (
isRunning bool isRunning bool
hostname string hostname string
hashStorage *global.HashStorage hashStorage *global.HashStorage
handler *th.Handler
// clients data to adding new client // clients data to adding new client
receiver_inbound_ID int receiver_inbound_ID int
@@ -148,7 +151,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
} }
// After bot initialization, set up bot commands with localized descriptions // After bot initialization, set up bot commands with localized descriptions
err = bot.SetMyCommands(&telego.SetMyCommandsParams{ err = bot.SetMyCommands(context.Background(), &telego.SetMyCommandsParams{
Commands: []telego.BotCommand{ Commands: []telego.BotCommand{
{Command: "start", Description: t.I18nBot("tgbot.commands.startDesc")}, {Command: "start", Description: t.I18nBot("tgbot.commands.startDesc")},
{Command: "help", Description: t.I18nBot("tgbot.commands.helpDesc")}, {Command: "help", Description: t.I18nBot("tgbot.commands.helpDesc")},
@@ -221,8 +224,9 @@ func (t *Tgbot) SetHostname() {
} }
func (t *Tgbot) Stop() { func (t *Tgbot) Stop() {
botHandler.Stop() if botHandler != nil {
bot.StopLongPolling() botHandler.Stop()
}
logger.Info("Stop Telegram receiver ...") logger.Info("Stop Telegram receiver ...")
isRunning = false isRunning = false
adminIds = nil adminIds = nil
@@ -255,26 +259,29 @@ func (t *Tgbot) OnReceive() {
Timeout: 10, Timeout: 10,
} }
updates, _ := bot.UpdatesViaLongPolling(&params) updates, _ := bot.UpdatesViaLongPolling(context.Background(), &params)
botHandler, _ = th.NewBotHandler(bot, updates) botHandler, _ = th.NewBotHandler(bot, updates)
botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) { botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove()) t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
return nil
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard"))) }, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) { botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID)) t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
return nil
}, th.AnyCommand()) }, th.AnyCommand())
botHandler.HandleCallbackQuery(func(_ *telego.Bot, query telego.CallbackQuery) { botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
delete(userStates, query.Message.GetChat().ID) delete(userStates, query.Message.GetChat().ID)
t.answerCallback(&query, checkAdmin(query.From.ID)) t.answerCallback(&query, checkAdmin(query.From.ID))
return nil
}, th.AnyCallbackQueryWithMessage()) }, th.AnyCallbackQueryWithMessage())
botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) { botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
if userState, exists := userStates[message.Chat.ID]; exists { if userState, exists := userStates[message.Chat.ID]; exists {
switch userState { switch userState {
case "awaiting_id": case "awaiting_id":
@@ -284,7 +291,7 @@ func (t *Tgbot) OnReceive() {
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text) t.addClient(message.Chat.ID, message_text)
return return nil
} }
client_Id = strings.TrimSpace(message.Text) client_Id = strings.TrimSpace(message.Text)
@@ -309,7 +316,7 @@ func (t *Tgbot) OnReceive() {
if client_TrPassword == strings.TrimSpace(message.Text) { if client_TrPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
return return nil
} }
client_TrPassword = strings.TrimSpace(message.Text) client_TrPassword = strings.TrimSpace(message.Text)
@@ -334,7 +341,7 @@ func (t *Tgbot) OnReceive() {
if client_ShPassword == strings.TrimSpace(message.Text) { if client_ShPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
return return nil
} }
client_ShPassword = strings.TrimSpace(message.Text) client_ShPassword = strings.TrimSpace(message.Text)
@@ -359,7 +366,7 @@ func (t *Tgbot) OnReceive() {
if client_Email == strings.TrimSpace(message.Text) { if client_Email == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
return return nil
} }
client_Email = strings.TrimSpace(message.Text) client_Email = strings.TrimSpace(message.Text)
@@ -384,7 +391,7 @@ func (t *Tgbot) OnReceive() {
if client_Comment == strings.TrimSpace(message.Text) { if client_Comment == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
return return nil
} }
client_Comment = strings.TrimSpace(message.Text) client_Comment = strings.TrimSpace(message.Text)
@@ -417,6 +424,7 @@ func (t *Tgbot) OnReceive() {
} }
} }
} }
return nil
}, th.AnyMessage()) }, th.AnyMessage())
botHandler.Start() botHandler.Start()
@@ -635,13 +643,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 4 { if len(dataArray) == 4 {
num, err := strconv.Atoi(dataArray[3]) num, err := strconv.Atoi(dataArray[3])
if err == nil { if err == nil {
if num == -2 { switch num {
case -2:
inputNumber = 0 inputNumber = 0
} else if num == -1 { case -1:
if inputNumber > 0 { if inputNumber > 0 {
inputNumber = (inputNumber / 10) inputNumber = (inputNumber / 10)
} }
} else { default:
inputNumber = (inputNumber * 10) + num inputNumber = (inputNumber * 10) + num
} }
} }
@@ -698,8 +707,12 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return return
} }
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(chatId, message_text, messageId) t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
case "add_client_limit_traffic_in": case "add_client_limit_traffic_in":
if len(dataArray) >= 2 { if len(dataArray) >= 2 {
@@ -709,13 +722,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 3 { if len(dataArray) == 3 {
num, err := strconv.Atoi(dataArray[2]) num, err := strconv.Atoi(dataArray[2])
if err == nil { if err == nil {
if num == -2 { switch num {
case -2:
inputNumber = 0 inputNumber = 0
} else if num == -1 { case -1:
if inputNumber > 0 { if inputNumber > 0 {
inputNumber = (inputNumber / 10) inputNumber = (inputNumber / 10)
} }
} else { default:
inputNumber = (inputNumber * 10) + num inputNumber = (inputNumber * 10) + num
} }
} }
@@ -838,13 +852,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 4 { if len(dataArray) == 4 {
num, err := strconv.Atoi(dataArray[3]) num, err := strconv.Atoi(dataArray[3])
if err == nil { if err == nil {
if num == -2 { switch num {
case -2:
inputNumber = 0 inputNumber = 0
} else if num == -1 { case -1:
if inputNumber > 0 { if inputNumber > 0 {
inputNumber = (inputNumber / 10) inputNumber = (inputNumber / 10)
} }
} else { default:
inputNumber = (inputNumber * 10) + num inputNumber = (inputNumber * 10) + num
} }
} }
@@ -913,8 +928,12 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return return
} }
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(chatId, message_text, messageId) t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
case "add_client_reset_exp_in": case "add_client_reset_exp_in":
if len(dataArray) >= 2 { if len(dataArray) >= 2 {
@@ -924,13 +943,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 3 { if len(dataArray) == 3 {
num, err := strconv.Atoi(dataArray[2]) num, err := strconv.Atoi(dataArray[2])
if err == nil { if err == nil {
if num == -2 { switch num {
case -2:
inputNumber = 0 inputNumber = 0
} else if num == -1 { case -1:
if inputNumber > 0 { if inputNumber > 0 {
inputNumber = (inputNumber / 10) inputNumber = (inputNumber / 10)
} }
} else { default:
inputNumber = (inputNumber * 10) + num inputNumber = (inputNumber * 10) + num
} }
} }
@@ -1029,13 +1049,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 4 { if len(dataArray) == 4 {
num, err := strconv.Atoi(dataArray[3]) num, err := strconv.Atoi(dataArray[3])
if err == nil { if err == nil {
if num == -2 { switch num {
case -2:
inputNumber = 0 inputNumber = 0
} else if num == -1 { case -1:
if inputNumber > 0 { if inputNumber > 0 {
inputNumber = (inputNumber / 10) inputNumber = (inputNumber / 10)
} }
} else { default:
inputNumber = (inputNumber * 10) + num inputNumber = (inputNumber * 10) + num
} }
} }
@@ -1095,8 +1116,12 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return return
} }
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(chatId, message_text, messageId) t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
case "add_client_ip_limit_in": case "add_client_ip_limit_in":
if len(dataArray) >= 2 { if len(dataArray) >= 2 {
@@ -1106,13 +1131,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 3 { if len(dataArray) == 3 {
num, err := strconv.Atoi(dataArray[2]) num, err := strconv.Atoi(dataArray[2])
if err == nil { if err == nil {
if num == -2 { switch num {
case -2:
inputNumber = 0 inputNumber = 0
} else if num == -1 { case -1:
if inputNumber > 0 { if inputNumber > 0 {
inputNumber = (inputNumber / 10) inputNumber = (inputNumber / 10)
} }
} else { default:
inputNumber = (inputNumber * 10) + num inputNumber = (inputNumber * 10) + num
} }
} }
@@ -1157,8 +1183,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return return
} }
} }
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
case "clear_ips": case "clear_ips":
inlineKeyboard := tu.InlineKeyboard( inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
@@ -1284,8 +1308,12 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
} }
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(chatId, message_text) t.addClient(callbackQuery.Message.GetChat().ID, message_text)
} }
return return
} else { } else {
@@ -1330,6 +1358,73 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
case "client_commands": case "client_commands":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands"))
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpClientCommands")) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpClientCommands"))
case "client_sub_links":
// show user's own clients to choose one for sub links
tgUserID := callbackQuery.From.ID
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
if err != nil {
// fallback to message
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
return
}
if len(traffics) == 0 {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10)))
return
}
var buttons []telego.InlineKeyboardButton
for _, tr := range traffics {
buttons = append(buttons, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery("client_sub_links "+tr.Email)))
}
cols := 1
if len(buttons) >= 6 {
cols = 2
}
keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), keyboard)
case "client_individual_links":
// show user's clients to choose for individual links
tgUserID := callbackQuery.From.ID
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
if err != nil {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
return
}
if len(traffics) == 0 {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10)))
return
}
var buttons2 []telego.InlineKeyboardButton
for _, tr := range traffics {
buttons2 = append(buttons2, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery("client_individual_links "+tr.Email)))
}
cols2 := 1
if len(buttons2) >= 6 {
cols2 = 2
}
keyboard2 := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols2, buttons2...))
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), keyboard2)
case "client_qr_links":
// show user's clients to choose for QR codes
tgUserID := callbackQuery.From.ID
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
if err != nil {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOccurred")+"\r\n"+err.Error())
return
}
if len(traffics) == 0 {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10)))
return
}
var buttons3 []telego.InlineKeyboardButton
for _, tr := range traffics {
buttons3 = append(buttons3, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery("client_qr_links "+tr.Email)))
}
cols3 := 1
if len(buttons3) >= 6 {
cols3 = 2
}
keyboard3 := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols3, buttons3...))
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), keyboard3)
case "onlines": case "onlines":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.onlines")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.onlines"))
t.onlineClients(chatId) t.onlineClients(chatId)
@@ -1414,6 +1509,23 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
) )
prompt_message := t.I18nBot("tgbot.messages.comment_prompt", "ClientComment=="+client_Comment) prompt_message := t.I18nBot("tgbot.messages.comment_prompt", "ClientComment=="+client_Comment)
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup) t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
default:
// dynamic callbacks
if strings.HasPrefix(callbackQuery.Data, "client_sub_links ") {
email := strings.TrimPrefix(callbackQuery.Data, "client_sub_links ")
t.sendClientSubLinks(chatId, email)
return
}
if strings.HasPrefix(callbackQuery.Data, "client_individual_links ") {
email := strings.TrimPrefix(callbackQuery.Data, "client_individual_links ")
t.sendClientIndividualLinks(chatId, email)
return
}
if strings.HasPrefix(callbackQuery.Data, "client_qr_links ") {
email := strings.TrimPrefix(callbackQuery.Data, "client_qr_links ")
t.sendClientQRLinks(chatId, email)
return
}
case "add_client_ch_default_traffic": case "add_client_ch_default_traffic":
inlineKeyboard := tu.InlineKeyboard( inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
@@ -1520,6 +1632,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return return
} }
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(chatId, message_text, messageId) t.addClient(chatId, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
case "add_client_default_ip_limit": case "add_client_default_ip_limit":
@@ -1530,6 +1646,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return return
} }
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(chatId, message_text, messageId) t.addClient(chatId, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
case "add_client_submit_disable": case "add_client_submit_disable":
@@ -1594,6 +1714,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return return
} }
valid_emails, extra_emails, err := t.inboundService.FilterAndSortClientEmails(emails) valid_emails, extra_emails, err := t.inboundService.FilterAndSortClientEmails(emails)
if err != nil {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove())
return
}
for _, valid_emails := range valid_emails { for _, valid_emails := range valid_emails {
traffic, err := t.inboundService.GetClientTrafficByEmail(valid_emails) traffic, err := t.inboundService.GetClientTrafficByEmail(valid_emails)
@@ -1756,6 +1880,10 @@ func (t *Tgbot) SubmitAddClient() (bool, error) {
} }
jsonString, err := t.BuildJSONForProtocol(inbound.Protocol) jsonString, err := t.BuildJSONForProtocol(inbound.Protocol)
if err != nil {
logger.Warning("BuildJSONForProtocol run failed:", err)
return false, errors.New("failed to build JSON for protocol")
}
newInbound := &model.Inbound{ newInbound := &model.Inbound{
Id: receiver_inbound_ID, Id: receiver_inbound_ID,
@@ -1806,6 +1934,13 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.clientUsage")).WithCallbackData(t.encodeQuery("client_traffic")), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.clientUsage")).WithCallbackData(t.encodeQuery("client_traffic")),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("client_commands")), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("client_commands")),
), ),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("pages.settings.subSettings")).WithCallbackData(t.encodeQuery("client_sub_links")),
tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("client_individual_links")),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("qrCode")).WithCallbackData(t.encodeQuery("client_qr_links")),
),
) )
var ReplyMarkup telego.ReplyMarkup var ReplyMarkup telego.ReplyMarkup
@@ -1859,7 +1994,7 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
if len(replyMarkup) > 0 && n == (len(allMessages)-1) { if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
params.ReplyMarkup = replyMarkup[0] params.ReplyMarkup = replyMarkup[0]
} }
_, err := bot.SendMessage(&params) _, err := bot.SendMessage(context.Background(), &params)
if err != nil { if err != nil {
logger.Warning("Error sending telegram message :", err) logger.Warning("Error sending telegram message :", err)
} }
@@ -1867,6 +2002,255 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
} }
} }
// buildSubscriptionURLs builds the HTML sub page URL and JSON subscription URL for a client email
func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
// Resolve subId from client email
traffic, client, err := t.inboundService.GetClientByEmail(email)
_ = traffic
if err != nil || client == nil {
return "", "", errors.New("client not found")
}
// Gather settings to construct absolute URLs
subDomain, _ := t.settingService.GetSubDomain()
subPort, _ := t.settingService.GetSubPort()
subPath, _ := t.settingService.GetSubPath()
subJsonPath, _ := t.settingService.GetSubJsonPath()
subKeyFile, _ := t.settingService.GetSubKeyFile()
subCertFile, _ := t.settingService.GetSubCertFile()
tls := (subKeyFile != "" && subCertFile != "")
scheme := "http"
if tls {
scheme = "https"
}
// Fallbacks
if subDomain == "" {
// try panel domain, otherwise OS hostname
if d, err := t.settingService.GetWebDomain(); err == nil && d != "" {
subDomain = d
} else if hostname != "" {
subDomain = hostname
} else {
subDomain = "localhost"
}
}
host := subDomain
if (subPort == 443 && tls) || (subPort == 80 && !tls) {
// standard ports: no port in host
} else {
host = fmt.Sprintf("%s:%d", subDomain, subPort)
}
// Ensure paths
if !strings.HasPrefix(subPath, "/") {
subPath = "/" + subPath
}
if !strings.HasSuffix(subPath, "/") {
subPath = subPath + "/"
}
if !strings.HasPrefix(subJsonPath, "/") {
subJsonPath = "/" + subJsonPath
}
if !strings.HasSuffix(subJsonPath, "/") {
subJsonPath = subJsonPath + "/"
}
subURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
subJsonURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
return subURL, subJsonURL, nil
}
func (t *Tgbot) sendClientSubLinks(chatId int64, email string) {
subURL, subJsonURL, err := t.buildSubscriptionURLs(email)
if err != nil {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
return
}
msg := "Subscription URL:\r\n<code>" + subURL + "</code>\r\n\r\n" +
"JSON URL:\r\n<code>" + subJsonURL + "</code>"
inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("client_individual_links " + email)),
),
)
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
}
// sendClientIndividualLinks fetches the subscription content (individual links) and sends it to the user
func (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) {
// Build the HTML sub page URL; we'll call it with header Accept to get raw content
subURL, _, err := t.buildSubscriptionURLs(email)
if err != nil {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
return
}
// Try to fetch raw subscription links. Prefer plain text response.
req, err := http.NewRequest("GET", subURL, nil)
if err != nil {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
return
}
// Force plain text to avoid HTML page; controller respects Accept header
req.Header.Set("Accept", "text/plain, */*;q=0.1")
// Use default client with reasonable timeout via context
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
return
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
return
}
// If service is configured to encode (Base64), decode it
encoded, _ := t.settingService.GetSubEncrypt()
var content string
if encoded {
decoded, err := base64.StdEncoding.DecodeString(string(bodyBytes))
if err != nil {
// fallback to raw text
content = string(bodyBytes)
} else {
content = string(decoded)
}
} else {
content = string(bodyBytes)
}
// Normalize line endings and trim
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
var cleaned []string
for _, l := range lines {
l = strings.TrimSpace(l)
if l != "" {
cleaned = append(cleaned, l)
}
}
if len(cleaned) == 0 {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.noResult"))
return
}
// Send in chunks to respect message length; use monospace formatting
const maxPerMessage = 50
for i := 0; i < len(cleaned); i += maxPerMessage {
j := i + maxPerMessage
if j > len(cleaned) {
j = len(cleaned)
}
chunk := cleaned[i:j]
msg := t.I18nBot("subscription.individualLinks") + ":\r\n"
for _, link := range chunk {
// wrap each link in <code>
msg += "<code>" + link + "</code>\r\n"
}
t.SendMsgToTgbot(chatId, msg)
}
}
// sendClientQRLinks generates QR images for subscription URL, JSON URL, and a few individual links, then sends them
func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
subURL, subJsonURL, err := t.buildSubscriptionURLs(email)
if err != nil {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
return
}
// Helper to create QR PNG bytes from content
createQR := func(content string, size int) ([]byte, error) {
if size <= 0 {
size = 256
}
return qrcode.Encode(content, qrcode.Medium, size)
}
// Inform user
t.SendMsgToTgbot(chatId, "QRCode"+":")
// Send sub URL QR (filename: sub.png)
if png, err := createQR(subURL, 320); err == nil {
document := tu.Document(
tu.ID(chatId),
tu.FileFromBytes(png, "sub.png"),
)
_, _ = bot.SendDocument(context.Background(), document)
} else {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
}
// Send JSON URL QR (filename: subjson.png)
if png, err := createQR(subJsonURL, 320); err == nil {
document := tu.Document(
tu.ID(chatId),
tu.FileFromBytes(png, "subjson.png"),
)
_, _ = bot.SendDocument(context.Background(), document)
} else {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
}
// Also generate a few individual links' QRs (first up to 5)
subPageURL := subURL
req, err := http.NewRequest("GET", subPageURL, nil)
if err == nil {
req.Header.Set("Accept", "text/plain, */*;q=0.1")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req = req.WithContext(ctx)
if resp, err := http.DefaultClient.Do(req); err == nil {
body, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close()
encoded, _ := t.settingService.GetSubEncrypt()
var content string
if encoded {
if dec, err := base64.StdEncoding.DecodeString(string(body)); err == nil {
content = string(dec)
} else {
content = string(body)
}
} else {
content = string(body)
}
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
var cleaned []string
for _, l := range lines {
l = strings.TrimSpace(l)
if l != "" {
cleaned = append(cleaned, l)
}
}
if len(cleaned) > 0 {
max := min(len(cleaned), 5)
for i := range max {
if png, err := createQR(cleaned[i], 320); err == nil {
// Use the email as filename for individual link QR
filename := email + ".png"
document := tu.Document(
tu.ID(chatId),
tu.FileFromBytes(png, filename),
)
_, _ = bot.SendDocument(context.Background(), document)
time.Sleep(200 * time.Millisecond)
}
}
}
}
}
}
func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) { func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) {
if len(replyMarkup) > 0 { if len(replyMarkup) > 0 {
for _, adminId := range adminIds { for _, adminId := range adminIds {
@@ -2004,10 +2388,11 @@ func (t *Tgbot) UserLoginNotify(username string, password string, ip string, tim
} }
msg := "" msg := ""
if status == LoginSuccess { switch status {
case LoginSuccess:
msg += t.I18nBot("tgbot.messages.loginSuccess") msg += t.I18nBot("tgbot.messages.loginSuccess")
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
} else if status == LoginFail { case LoginFail:
msg += t.I18nBot("tgbot.messages.loginFailed") msg += t.I18nBot("tgbot.messages.loginFailed")
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
msg += t.I18nBot("tgbot.messages.password", "Password=="+password) msg += t.I18nBot("tgbot.messages.password", "Password=="+password)
@@ -2087,8 +2472,8 @@ func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
} }
excludedProtocols := map[model.Protocol]bool{ excludedProtocols := map[model.Protocol]bool{
model.DOKODEMO: true, model.Tunnel: true,
model.Socks: true, model.Mixed: true,
model.WireGuard: true, model.WireGuard: true,
model.HTTP: true, model.HTTP: true,
} }
@@ -2167,6 +2552,22 @@ func (t *Tgbot) clientInfoMsg(
expiryTime = t.I18nBot("tgbot.unlimited") expiryTime = t.I18nBot("tgbot.unlimited")
} else if diff > 172800 || !traffic.Enable { } else if diff > 172800 || !traffic.Enable {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
if diff > 0 {
days := diff / 86400
hours := (diff % 86400) / 3600
minutes := (diff % 3600) / 60
remainingTime := ""
if days > 0 {
remainingTime += fmt.Sprintf("%d %s ", days, t.I18nBot("tgbot.days"))
}
if hours > 0 {
remainingTime += fmt.Sprintf("%d %s ", hours, t.I18nBot("tgbot.hours"))
}
if minutes > 0 {
remainingTime += fmt.Sprintf("%d %s", minutes, t.I18nBot("tgbot.minutes"))
}
expiryTime += fmt.Sprintf(" (%s)", remainingTime)
}
} else if traffic.ExpiryTime < 0 { } else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
flag = true flag = true
@@ -2765,7 +3166,7 @@ func (t *Tgbot) sendBackup(chatId int64) {
tu.ID(chatId), tu.ID(chatId),
tu.File(file), tu.File(file),
) )
_, err = bot.SendDocument(document) _, err = bot.SendDocument(context.Background(), document)
if err != nil { if err != nil {
logger.Error("Error in uploading backup: ", err) logger.Error("Error in uploading backup: ", err)
} }
@@ -2779,7 +3180,7 @@ func (t *Tgbot) sendBackup(chatId int64) {
tu.ID(chatId), tu.ID(chatId),
tu.File(file), tu.File(file),
) )
_, err = bot.SendDocument(document) _, err = bot.SendDocument(context.Background(), document)
if err != nil { if err != nil {
logger.Error("Error in uploading config.json: ", err) logger.Error("Error in uploading config.json: ", err)
} }
@@ -2803,7 +3204,7 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
tu.ID(chatId), tu.ID(chatId),
tu.File(file), tu.File(file),
) )
_, err = bot.SendDocument(document) _, err = bot.SendDocument(context.Background(), document)
if err != nil { if err != nil {
logger.Error("Error in uploading IPLimitBannedPrevLog: ", err) logger.Error("Error in uploading IPLimitBannedPrevLog: ", err)
} }
@@ -2824,7 +3225,7 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
tu.ID(chatId), tu.ID(chatId),
tu.File(file), tu.File(file),
) )
_, err = bot.SendDocument(document) _, err = bot.SendDocument(context.Background(), document)
if err != nil { if err != nil {
logger.Error("Error in uploading IPLimitBannedLog: ", err) logger.Error("Error in uploading IPLimitBannedLog: ", err)
} }
@@ -2842,7 +3243,7 @@ func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
CallbackQueryID: id, CallbackQueryID: id,
Text: message, Text: message,
} }
if err := bot.AnswerCallbackQuery(&params); err != nil { if err := bot.AnswerCallbackQuery(context.Background(), &params); err != nil {
logger.Warning(err) logger.Warning(err)
} }
} }
@@ -2853,7 +3254,7 @@ func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyb
MessageID: messageID, MessageID: messageID,
ReplyMarkup: inlineKeyboard, ReplyMarkup: inlineKeyboard,
} }
if _, err := bot.EditMessageReplyMarkup(&params); err != nil { if _, err := bot.EditMessageReplyMarkup(context.Background(), &params); err != nil {
logger.Warning(err) logger.Warning(err)
} }
} }
@@ -2868,7 +3269,7 @@ func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlin
if len(inlineKeyboard) > 0 { if len(inlineKeyboard) > 0 {
params.ReplyMarkup = inlineKeyboard[0] params.ReplyMarkup = inlineKeyboard[0]
} }
if _, err := bot.EditMessageText(&params); err != nil { if _, err := bot.EditMessageText(context.Background(), &params); err != nil {
logger.Warning(err) logger.Warning(err)
} }
} }
@@ -2881,7 +3282,7 @@ func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSecon
} }
// Send the message // Send the message
sentMsg, err := bot.SendMessage(&telego.SendMessageParams{ sentMsg, err := bot.SendMessage(context.Background(), &telego.SendMessageParams{
ChatID: tu.ID(chatId), ChatID: tu.ID(chatId),
Text: msg, Text: msg,
ReplyMarkup: replyMarkupParam, // Use the correct replyMarkup value ReplyMarkup: replyMarkupParam, // Use the correct replyMarkup value
@@ -2904,7 +3305,7 @@ func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) {
ChatID: tu.ID(chatId), ChatID: tu.ID(chatId),
MessageID: messageID, MessageID: messageID,
} }
if err := bot.DeleteMessage(&params); err != nil { if err := bot.DeleteMessage(context.Background(), &params); err != nil {
logger.Warning("Failed to delete message:", err) logger.Warning("Failed to delete message:", err)
} else { } else {
logger.Info("Message deleted successfully") logger.Info("Message deleted successfully")

View File

@@ -3,6 +3,7 @@ package service
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"runtime"
"sync" "sync"
"x-ui/logger" "x-ui/logger"
@@ -14,7 +15,8 @@ import (
var ( var (
p *xray.Process p *xray.Process
lock sync.Mutex lock sync.Mutex
isNeedXrayRestart atomic.Bool isNeedXrayRestart atomic.Bool // Indicates that restart was requested for Xray
isManuallyStopped atomic.Bool // Indicates that Xray was stopped manually from the panel
result string result string
) )
@@ -32,7 +34,16 @@ func (s *XrayService) GetXrayErr() error {
if p == nil { if p == nil {
return nil return nil
} }
return p.GetErr()
err := p.GetErr()
if runtime.GOOS == "windows" && err.Error() == "exit status 1" {
// exit status 1 on Windows means that Xray process was killed
// as we kill process to stop in on Windows, this is not an error
return nil
}
return err
} }
func (s *XrayService) GetXrayResult() string { func (s *XrayService) GetXrayResult() string {
@@ -45,7 +56,15 @@ func (s *XrayService) GetXrayResult() string {
if p == nil { if p == nil {
return "" return ""
} }
result = p.GetResult() result = p.GetResult()
if runtime.GOOS == "windows" && result == "exit status 1" {
// exit status 1 on Windows means that Xray process was killed
// as we kill process to stop in on Windows, this is not an error
return ""
}
return result return result
} }
@@ -184,7 +203,8 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic,
func (s *XrayService) RestartXray(isForce bool) error { func (s *XrayService) RestartXray(isForce bool) error {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
logger.Debug("restart xray, force:", isForce) logger.Debug("restart Xray, force:", isForce)
isManuallyStopped.Store(false)
xrayConfig, err := s.GetXrayConfig() xrayConfig, err := s.GetXrayConfig()
if err != nil { if err != nil {
@@ -192,8 +212,8 @@ func (s *XrayService) RestartXray(isForce bool) error {
} }
if s.IsXrayRunning() { if s.IsXrayRunning() {
if !isForce && p.GetConfig().Equals(xrayConfig) { if !isForce && p.GetConfig().Equals(xrayConfig) && !isNeedXrayRestart.Load() {
logger.Debug("It does not need to restart xray") logger.Debug("It does not need to restart Xray")
return nil return nil
} }
p.Stop() p.Stop()
@@ -205,12 +225,14 @@ func (s *XrayService) RestartXray(isForce bool) error {
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (s *XrayService) StopXray() error { func (s *XrayService) StopXray() error {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
isManuallyStopped.Store(true)
logger.Debug("Attempting to stop Xray...") logger.Debug("Attempting to stop Xray...")
if s.IsXrayRunning() { if s.IsXrayRunning() {
return p.Stop() return p.Stop()
@@ -225,3 +247,8 @@ func (s *XrayService) SetToNeedRestart() {
func (s *XrayService) IsNeedRestartAndSetFalse() bool { func (s *XrayService) IsNeedRestartAndSetFalse() bool {
return isNeedXrayRestart.CompareAndSwap(true, false) return isNeedXrayRestart.CompareAndSwap(true, false)
} }
// Check if Xray is not running and wasn't stopped manually, i.e. crashed
func (s *XrayService) DidXrayCrash() bool {
return !s.IsXrayRunning() && !isManuallyStopped.Load()
}

View File

@@ -2,6 +2,7 @@ package session
import ( import (
"encoding/gob" "encoding/gob"
"net/http"
"x-ui/database/model" "x-ui/database/model"
@@ -32,6 +33,7 @@ func SetMaxAge(c *gin.Context, maxAge int) {
Path: defaultPath, Path: defaultPath,
MaxAge: maxAge, MaxAge: maxAge,
HttpOnly: true, HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}) })
} }
@@ -61,5 +63,6 @@ func ClearSession(c *gin.Context) {
Path: defaultPath, Path: defaultPath,
MaxAge: -1, MaxAge: -1,
HttpOnly: true, HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}) })
} }

View File

@@ -50,6 +50,7 @@
"fail" = "فشل" "fail" = "فشل"
"comment" = "تعليق" "comment" = "تعليق"
"success" = "تم بنجاح" "success" = "تم بنجاح"
"lastOnline" = "آخر متصل"
"getVersion" = "جيب النسخة" "getVersion" = "جيب النسخة"
"install" = "تثبيت" "install" = "تثبيت"
"clients" = "عملاء" "clients" = "عملاء"
@@ -71,6 +72,20 @@
"emptyReverseDesc" = "مفيش بروكسي عكسي مضاف." "emptyReverseDesc" = "مفيش بروكسي عكسي مضاف."
"somethingWentWrong" = "حدث خطأ ما" "somethingWentWrong" = "حدث خطأ ما"
[subscription]
"title" = "معلومات الاشتراك"
"subId" = "معرّف الاشتراك"
"status" = "الحالة"
"downloaded" = "التنزيل"
"uploaded" = "الرفع"
"expiry" = "تاريخ الانتهاء"
"totalQuota" = "الحصة الإجمالية"
"individualLinks" = "روابط فردية"
"active" = "نشط"
"inactive" = "غير نشط"
"unlimited" = "غير محدود"
"noExpiry" = "بدون انتهاء"
[menu] [menu]
"theme" = "الثيم" "theme" = "الثيم"
"dark" = "داكن" "dark" = "داكن"
@@ -132,6 +147,8 @@
"xraySwitchVersionPopover" = "تم تحديث Xray بنجاح" "xraySwitchVersionPopover" = "تم تحديث Xray بنجاح"
"geofileUpdateDialog" = "هل تريد حقًا تحديث ملف الجغرافيا؟" "geofileUpdateDialog" = "هل تريد حقًا تحديث ملف الجغرافيا؟"
"geofileUpdateDialogDesc" = "سيؤدي هذا إلى تحديث ملف #filename#." "geofileUpdateDialogDesc" = "سيؤدي هذا إلى تحديث ملف #filename#."
"geofilesUpdateDialogDesc" = "سيؤدي هذا إلى تحديث كافة الملفات."
"geofilesUpdateAll" = "تحديث الكل"
"geofileUpdatePopover" = "تم تحديث ملف الجغرافيا بنجاح" "geofileUpdatePopover" = "تم تحديث ملف الجغرافيا بنجاح"
"dontRefresh" = "التثبيت شغال، متعملش Refresh للصفحة" "dontRefresh" = "التثبيت شغال، متعملش Refresh للصفحة"
"logs" = "السجلات" "logs" = "السجلات"
@@ -149,6 +166,8 @@
"getConfigError" = "حدث خطأ أثناء استرجاع ملف الإعدادات" "getConfigError" = "حدث خطأ أثناء استرجاع ملف الإعدادات"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "إجمالي حركة المرور"
"allTimeTrafficUsage" = "إجمالي الاستخدام طوال الوقت"
"title" = "الإدخالات" "title" = "الإدخالات"
"totalDownUp" = "إجمالي المرسل/المستقبل" "totalDownUp" = "إجمالي المرسل/المستقبل"
"totalUsage" = "إجمالي الاستخدام" "totalUsage" = "إجمالي الاستخدام"
@@ -158,10 +177,13 @@
"remark" = "ملاحظة" "remark" = "ملاحظة"
"protocol" = "بروتوكول" "protocol" = "بروتوكول"
"port" = "بورت" "port" = "بورت"
"portMap" = "خريطة البورت"
"traffic" = "الترافيك" "traffic" = "الترافيك"
"details" = "تفاصيل" "details" = "تفاصيل"
"transportConfig" = "نقل" "transportConfig" = "نقل"
"expireDate" = "المدة" "expireDate" = "المدة"
"createdAt" = "تاريخ الإنشاء"
"updatedAt" = "تاريخ التحديث"
"resetTraffic" = "إعادة ضبط الترافيك" "resetTraffic" = "إعادة ضبط الترافيك"
"addInbound" = "أضف إدخال" "addInbound" = "أضف إدخال"
"generalActions" = "إجراءات عامة" "generalActions" = "إجراءات عامة"
@@ -173,8 +195,6 @@
"deleteClient" = "حذف العميل" "deleteClient" = "حذف العميل"
"deleteClientContent" = "متأكد إنك عايز تحذف العميل؟" "deleteClientContent" = "متأكد إنك عايز تحذف العميل؟"
"resetTrafficContent" = "متأكد إنك عايز تعيد ضبط الترافيك؟" "resetTrafficContent" = "متأكد إنك عايز تعيد ضبط الترافيك؟"
"inboundUpdateSuccess" = "تم تحديث الوارد بنجاح."
"inboundCreateSuccess" = "تم إنشاء الوارد بنجاح."
"copyLink" = "انسخ الرابط" "copyLink" = "انسخ الرابط"
"address" = "العنوان" "address" = "العنوان"
"network" = "الشبكة" "network" = "الشبكة"
@@ -260,6 +280,8 @@
"resetInboundClientTrafficSuccess" = "تم إعادة تعيين حركة المرور" "resetInboundClientTrafficSuccess" = "تم إعادة تعيين حركة المرور"
"trafficGetError" = "خطأ في الحصول على حركات المرور" "trafficGetError" = "خطأ في الحصول على حركات المرور"
"getNewX25519CertError" = "حدث خطأ أثناء الحصول على شهادة X25519." "getNewX25519CertError" = "حدث خطأ أثناء الحصول على شهادة X25519."
"getNewmldsa65Error" = "حدث خطاء في الحصول على mldsa65."
"getNewVlessEncError" = "حدث خطأ أثناء الحصول على VlessEnc."
[pages.inbounds.stream.general] [pages.inbounds.stream.general]
"request" = "طلب" "request" = "طلب"
@@ -559,24 +581,25 @@
"resetOutboundTrafficError" = "خطأ في إعادة تعيين حركات المرور الصادرة" "resetOutboundTrafficError" = "خطأ في إعادة تعيين حركات المرور الصادرة"
[tgbot] [tgbot]
"keyboardClosed" = "❌ الكيبورد المخصص اتقفلت!" "keyboardClosed" = "❌ لوحة المفاتيح مغلقة!"
"noResult" = "❗ مفيش نتيجة!" "noResult" = "❗ لا يوجد نتائج!"
"noQuery" = "❌ مش لاقي السؤال! استخدم الأمر تاني!" "noQuery" = "❌ لم يتم العثور على الاستعلام! يرجى استخدام الأمر مرة أخرى!"
"wentWrong" = "❌ حصل خطأ!" "wentWrong" = "❌ حدث خطأ ما!"
"noIpRecord" = "❗ مفيش سجل IP!" "noIpRecord" = "❗ لا يوجد سجل IP!"
"noInbounds" = "❗ مفيش إدخال متواجد!" "noInbounds" = "❗ لم يتم العثور على أي وارد!"
"unlimited" = "♾ غير محدود (إعادة ضبط)" "unlimited" = "♾ غير محدود (إعادة تعيين)"
"add" = "أضف" "add" = "إضافة"
"month" = "شهر" "month" = "شهر"
"months" = "شهور" "months" = "أشهر"
"day" = "يوم" "day" = "يوم"
"days" = "أيام" "days" = "أيام"
"hours" = "ساعات" "hours" = "ساعات"
"unknown" = "مش معروف" "minutes" = "دقائق"
"inbounds" = "الإدخالات" "unknown" = "غير معروف"
"inbounds" = "الواردات"
"clients" = "العملاء" "clients" = "العملاء"
"offline" = "🔴 أوفلاين" "offline" = "🔴 غير متصل"
"online" = "🟢 أونلاين" "online" = "🟢 متصل"
[tgbot.commands] [tgbot.commands]
"unknown" = "❗ أمر مش معروف." "unknown" = "❗ أمر مش معروف."
@@ -644,7 +667,6 @@
"refreshedOn" = "\r\n📋🔄 اتحدّث في: {{ .Time }}\r\n\r\n" "refreshedOn" = "\r\n📋🔄 اتحدّث في: {{ .Time }}\r\n\r\n"
"yes" = "✅ أيوه" "yes" = "✅ أيوه"
"no" = "❌ لأ" "no" = "❌ لأ"
"received_id" = "🔑📥 الـ ID اتحدث." "received_id" = "🔑📥 الـ ID اتحدث."
"received_password" = "🔑📥 الباسورد اتحدث." "received_password" = "🔑📥 الباسورد اتحدث."
"received_email" = "📧📥 الإيميل اتحدث." "received_email" = "📧📥 الإيميل اتحدث."
@@ -664,7 +686,6 @@
"FailedResetTraffic" = "📧 البريد الإلكتروني: {{ .ClientEmail }}\n🏁 النتيجة: ❌ فشل \n\n🛠 الخطأ: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 البريد الإلكتروني: {{ .ClientEmail }}\n🏁 النتيجة: ❌ فشل \n\n🛠 الخطأ: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 عملية إعادة ضبط الترافيك خلصت لكل العملاء." "FinishProcess" = "🔚 عملية إعادة ضبط الترافيك خلصت لكل العملاء."
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ اقفل الكيبورد" "closeKeyboard" = "❌ اقفل الكيبورد"
"cancel" = "❌ إلغاء" "cancel" = "❌ إلغاء"
@@ -698,7 +719,6 @@
"limitTraffic" = "🚧 حد الترافيك" "limitTraffic" = "🚧 حد الترافيك"
"getBanLogs" = "احصل على سجلات الحظر" "getBanLogs" = "احصل على سجلات الحظر"
"allClients" = "كل العملاء" "allClients" = "كل العملاء"
"addClient" = "إضافة عميل" "addClient" = "إضافة عميل"
"submitDisable" = "إرسال كمعطّل ☑️" "submitDisable" = "إرسال كمعطّل ☑️"
"submitEnable" = "إرسال كمفعّل ✅" "submitEnable" = "إرسال كمفعّل ✅"
@@ -710,7 +730,6 @@
"ResetAllTraffics" = "إعادة ضبط جميع الترافيك" "ResetAllTraffics" = "إعادة ضبط جميع الترافيك"
"SortedTrafficUsageReport" = "تقرير استخدام الترافيك المرتب" "SortedTrafficUsageReport" = "تقرير استخدام الترافيك المرتب"
[tgbot.answers] [tgbot.answers]
"successfulOperation" = "✅ العملية نجحت!" "successfulOperation" = "✅ العملية نجحت!"
"errorOperation" = "❗ حصل خطأ في العملية." "errorOperation" = "❗ حصل خطأ في العملية."

View File

@@ -50,6 +50,7 @@
"fail" = "Failed" "fail" = "Failed"
"comment" = "Comment" "comment" = "Comment"
"success" = "Successfully" "success" = "Successfully"
"lastOnline" = "Last Online"
"getVersion" = "Get Version" "getVersion" = "Get Version"
"install" = "Install" "install" = "Install"
"clients" = "Clients" "clients" = "Clients"
@@ -71,6 +72,20 @@
"emptyReverseDesc" = "No added reverse proxies." "emptyReverseDesc" = "No added reverse proxies."
"somethingWentWrong" = "Something went wrong" "somethingWentWrong" = "Something went wrong"
[subscription]
"title" = "Subscription info"
"subId" = "Subscription ID"
"status" = "Status"
"downloaded" = "Downloaded"
"uploaded" = "Uploaded"
"expiry" = "Expiry"
"totalQuota" = "Total quota"
"individualLinks" = "Individual links"
"active" = "Active"
"inactive" = "Inactive"
"unlimited" = "Unlimited"
"noExpiry" = "No expiry"
[menu] [menu]
"theme" = "Theme" "theme" = "Theme"
"dark" = "Dark" "dark" = "Dark"
@@ -132,6 +147,8 @@
"xraySwitchVersionPopover" = "Xray updated successfully" "xraySwitchVersionPopover" = "Xray updated successfully"
"geofileUpdateDialog" = "Do you really want to update the geofile?" "geofileUpdateDialog" = "Do you really want to update the geofile?"
"geofileUpdateDialogDesc" = "This will update the #filename# file." "geofileUpdateDialogDesc" = "This will update the #filename# file."
"geofilesUpdateDialogDesc" = "This will update all geofiles."
"geofilesUpdateAll" = "Update all"
"geofileUpdatePopover" = "Geofile updated successfully" "geofileUpdatePopover" = "Geofile updated successfully"
"dontRefresh" = "Installation is in progress, please do not refresh this page" "dontRefresh" = "Installation is in progress, please do not refresh this page"
"logs" = "Logs" "logs" = "Logs"
@@ -149,6 +166,8 @@
"getConfigError" = "An error occurred while retrieving the config file." "getConfigError" = "An error occurred while retrieving the config file."
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "All-time Traffic"
"allTimeTrafficUsage" = "All Time Total Usage"
"title" = "Inbounds" "title" = "Inbounds"
"totalDownUp" = "Total Sent/Received" "totalDownUp" = "Total Sent/Received"
"totalUsage" = "Total Usage" "totalUsage" = "Total Usage"
@@ -158,10 +177,13 @@
"remark" = "Remark" "remark" = "Remark"
"protocol" = "Protocol" "protocol" = "Protocol"
"port" = "Port" "port" = "Port"
"portMap" = "Port Mapping"
"traffic" = "Traffic" "traffic" = "Traffic"
"details" = "Details" "details" = "Details"
"transportConfig" = "Transport" "transportConfig" = "Transport"
"expireDate" = "Duration" "expireDate" = "Duration"
"createdAt" = "Created"
"updatedAt" = "Updated"
"resetTraffic" = "Reset Traffic" "resetTraffic" = "Reset Traffic"
"addInbound" = "Add Inbound" "addInbound" = "Add Inbound"
"generalActions" = "General Actions" "generalActions" = "General Actions"
@@ -258,7 +280,8 @@
"resetInboundClientTrafficSuccess" = "Traffic has been reset." "resetInboundClientTrafficSuccess" = "Traffic has been reset."
"trafficGetError" = "Error getting traffics." "trafficGetError" = "Error getting traffics."
"getNewX25519CertError" = "Error while obtaining the X25519 certificate." "getNewX25519CertError" = "Error while obtaining the X25519 certificate."
"getNewmldsa65Error" = "Error while obtaining mldsa65."
"getNewVlessEncError" = "Error while obtaining VlessEnc."
[pages.inbounds.stream.general] [pages.inbounds.stream.general]
"request" = "Request" "request" = "Request"
@@ -571,6 +594,7 @@
"day" = "Day" "day" = "Day"
"days" = "Days" "days" = "Days"
"hours" = "Hours" "hours" = "Hours"
"minutes" = "Minutes"
"unknown" = "Unknown" "unknown" = "Unknown"
"inbounds" = "Inbounds" "inbounds" = "Inbounds"
"clients" = "Clients" "clients" = "Clients"
@@ -643,7 +667,6 @@
"refreshedOn" = "\r\n📋🔄 Refreshed On: {{ .Time }}\r\n\r\n" "refreshedOn" = "\r\n📋🔄 Refreshed On: {{ .Time }}\r\n\r\n"
"yes" = "✅ Yes" "yes" = "✅ Yes"
"no" = "❌ No" "no" = "❌ No"
"received_id" = "🔑📥 ID updated." "received_id" = "🔑📥 ID updated."
"received_password" = "🔑📥 Password updated." "received_password" = "🔑📥 Password updated."
"received_email" = "📧📥 Email updated." "received_email" = "📧📥 Email updated."
@@ -663,7 +686,6 @@
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Result: ❌ Failed \n\n🛠 Error: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Result: ❌ Failed \n\n🛠 Error: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Traffic reset process finished for all clients." "FinishProcess" = "🔚 Traffic reset process finished for all clients."
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ Close Keyboard" "closeKeyboard" = "❌ Close Keyboard"
"cancel" = "❌ Cancel" "cancel" = "❌ Cancel"
@@ -697,7 +719,6 @@
"limitTraffic" = "🚧 Traffic Limit" "limitTraffic" = "🚧 Traffic Limit"
"getBanLogs" = "Get Ban Logs" "getBanLogs" = "Get Ban Logs"
"allClients" = "All Clients" "allClients" = "All Clients"
"addClient" = "Add Client" "addClient" = "Add Client"
"submitDisable" = "Submit As Disable ☑️" "submitDisable" = "Submit As Disable ☑️"
"submitEnable" = "Submit As Enable ✅" "submitEnable" = "Submit As Enable ✅"

View File

@@ -50,6 +50,7 @@
"fail" = "Falló" "fail" = "Falló"
"comment" = "Comentario" "comment" = "Comentario"
"success" = "Éxito" "success" = "Éxito"
"lastOnline" = "Última conexión"
"getVersion" = "Obtener versión" "getVersion" = "Obtener versión"
"install" = "Instalar" "install" = "Instalar"
"clients" = "Clientes" "clients" = "Clientes"
@@ -71,6 +72,20 @@
"emptyReverseDesc" = "No hay proxies inversos añadidos." "emptyReverseDesc" = "No hay proxies inversos añadidos."
"somethingWentWrong" = "Algo salió mal" "somethingWentWrong" = "Algo salió mal"
[subscription]
"title" = "Información de suscripción"
"subId" = "ID de suscripción"
"status" = "Estado"
"downloaded" = "Descargado"
"uploaded" = "Subido"
"expiry" = "Caducidad"
"totalQuota" = "Cuota total"
"individualLinks" = "Enlaces individuales"
"active" = "Activo"
"inactive" = "Inactivo"
"unlimited" = "Ilimitado"
"noExpiry" = "Sin caducidad"
[menu] [menu]
"theme" = "Tema" "theme" = "Tema"
"dark" = "Oscuro" "dark" = "Oscuro"
@@ -91,7 +106,7 @@
"invalidFormData" = "El formato de los datos de entrada es inválido." "invalidFormData" = "El formato de los datos de entrada es inválido."
"emptyUsername" = "Por favor ingresa el nombre de usuario." "emptyUsername" = "Por favor ingresa el nombre de usuario."
"emptyPassword" = "Por favor ingresa la contraseña." "emptyPassword" = "Por favor ingresa la contraseña."
"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto." "wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto."
"successLogin" = "Has iniciado sesión en tu cuenta correctamente." "successLogin" = "Has iniciado sesión en tu cuenta correctamente."
[pages.index] [pages.index]
@@ -117,8 +132,6 @@
"operationHours" = "Tiempo de Funcionamiento" "operationHours" = "Tiempo de Funcionamiento"
"systemLoad" = "Carga del Sistema" "systemLoad" = "Carga del Sistema"
"systemLoadDesc" = "promedio de carga del sistema en los últimos 1, 5 y 15 minutos" "systemLoadDesc" = "promedio de carga del sistema en los últimos 1, 5 y 15 minutos"
"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" "connectionCount" = "Número de Conexiones"
"ipAddresses" = "Direcciones IP" "ipAddresses" = "Direcciones IP"
"toggleIpVisibility" = "Alternar visibilidad de la IP" "toggleIpVisibility" = "Alternar visibilidad de la IP"
@@ -134,6 +147,8 @@
"xraySwitchVersionPopover" = "Xray se actualizó correctamente" "xraySwitchVersionPopover" = "Xray se actualizó correctamente"
"geofileUpdateDialog" = "¿Realmente deseas actualizar el geofichero?" "geofileUpdateDialog" = "¿Realmente deseas actualizar el geofichero?"
"geofileUpdateDialogDesc" = "Esto actualizará el archivo #filename#." "geofileUpdateDialogDesc" = "Esto actualizará el archivo #filename#."
"geofilesUpdateDialogDesc" = "Esto actualizará todos los archivos."
"geofilesUpdateAll" = "Actualizar todo"
"geofileUpdatePopover" = "Geofichero actualizado correctamente" "geofileUpdatePopover" = "Geofichero actualizado correctamente"
"dontRefresh" = "La instalación está en progreso, por favor no actualices esta página." "dontRefresh" = "La instalación está en progreso, por favor no actualices esta página."
"logs" = "Registros" "logs" = "Registros"
@@ -151,6 +166,8 @@
"getConfigError" = "Ocurrió un error al obtener el archivo de configuración" "getConfigError" = "Ocurrió un error al obtener el archivo de configuración"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Tráfico Total"
"allTimeTrafficUsage" = "Uso total de todos los tiempos"
"title" = "Entradas" "title" = "Entradas"
"totalDownUp" = "Subidas/Descargas Totales" "totalDownUp" = "Subidas/Descargas Totales"
"totalUsage" = "Uso Total" "totalUsage" = "Uso Total"
@@ -160,10 +177,13 @@
"remark" = "Notas" "remark" = "Notas"
"protocol" = "Protocolo" "protocol" = "Protocolo"
"port" = "Puerto" "port" = "Puerto"
"portMap" = "Puertos de Destino"
"traffic" = "Tráfico" "traffic" = "Tráfico"
"details" = "Detalles" "details" = "Detalles"
"transportConfig" = "Transporte" "transportConfig" = "Transporte"
"expireDate" = "Fecha de Expiración" "expireDate" = "Fecha de Expiración"
"createdAt" = "Creado"
"updatedAt" = "Actualizado"
"resetTraffic" = "Restablecer Tráfico" "resetTraffic" = "Restablecer Tráfico"
"addInbound" = "Agregar Entrada" "addInbound" = "Agregar Entrada"
"generalActions" = "Acciones Generales" "generalActions" = "Acciones Generales"
@@ -175,8 +195,6 @@
"deleteClient" = "Eliminar cliente" "deleteClient" = "Eliminar cliente"
"deleteClientContent" = "¿Está seguro de que desea eliminar el cliente?" "deleteClientContent" = "¿Está seguro de que desea eliminar el cliente?"
"resetTrafficContent" = "¿Confirmar restablecimiento de tráfico?" "resetTrafficContent" = "¿Confirmar restablecimiento de tráfico?"
"inboundUpdateSuccess" = "La entrada se ha actualizado correctamente."
"inboundCreateSuccess" = "La entrada se ha creado correctamente."
"copyLink" = "Copiar Enlace" "copyLink" = "Copiar Enlace"
"address" = "Dirección" "address" = "Dirección"
"network" = "Red" "network" = "Red"
@@ -262,6 +280,8 @@
"resetInboundClientTrafficSuccess" = "El tráfico ha sido reiniciado" "resetInboundClientTrafficSuccess" = "El tráfico ha sido reiniciado"
"trafficGetError" = "Error al obtener los tráficos" "trafficGetError" = "Error al obtener los tráficos"
"getNewX25519CertError" = "Error al obtener el certificado X25519." "getNewX25519CertError" = "Error al obtener el certificado X25519."
"getNewmldsa65Error" = "Error al obtener el certificado mldsa65."
"getNewVlessEncError" = "Error al obtener el certificado VlessEnc."
[pages.inbounds.stream.general] [pages.inbounds.stream.general]
"request" = "Pedido" "request" = "Pedido"
@@ -535,9 +555,9 @@
[pages.settings.security] [pages.settings.security]
"admin" = "Credenciales de administrador" "admin" = "Credenciales de administrador"
"twoFactor" = "Autenticación de dos factores" "twoFactor" = "Autenticación de dos factores"
"twoFactorEnable" = "Habilitar 2FA" "twoFactorEnable" = "Habilitar 2FA"
"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad." "twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad."
"twoFactorModalSetTitle" = "Activar autenticación de dos factores" "twoFactorModalSetTitle" = "Activar autenticación de dos factores"
"twoFactorModalDeleteTitle" = "Desactivar autenticación de dos factores" "twoFactorModalDeleteTitle" = "Desactivar autenticación de dos factores"
"twoFactorModalSteps" = "Para configurar la autenticación de dos factores, sigue estos pasos:" "twoFactorModalSteps" = "Para configurar la autenticación de dos factores, sigue estos pasos:"
@@ -561,23 +581,24 @@
"resetOutboundTrafficError" = "Error al reiniciar el tráfico saliente" "resetOutboundTrafficError" = "Error al reiniciar el tráfico saliente"
[tgbot] [tgbot]
"keyboardClosed" = "❌ ¡Teclado personalizado cerrado!" "keyboardClosed" = "❌ Teclado cerrado!"
"noResult" = "❗ ¡Sin resultados!" "noResult" = "❗ ¡No hay resultados!"
"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor utiliza el comando nuevamente!" "noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor, use el comando de nuevo!"
"wentWrong" = "❌ ¡Algo salió mal!" "wentWrong" = "❌ ¡Algo salió mal!"
"noIpRecord" = "❗ ¡Sin Registro de IP!" "noIpRecord" = "❗ ¡No hay registro de IP!"
"noInbounds" = "❗ ¡No se encontraron entradas!" "noInbounds" = "❗ ¡No se encontraron entradas!"
"unlimited" = "♾ Ilimitado" "unlimited" = "♾ Ilimitado (Restablecer)"
"add" = "Agregar" "add" = "Añadir"
"month" = "Mes" "month" = "Mes"
"months" = "Meses" "months" = "Meses"
"day" = "Día" "day" = "Día"
"days" = "Días" "days" = "Días"
"hours" = "Horas" "hours" = "Horas"
"minutes" = "Minutos"
"unknown" = "Desconocido" "unknown" = "Desconocido"
"inbounds" = "Entradas" "inbounds" = "Entradas"
"clients" = "Clientes" "clients" = "Clientes"
"offline" = "🔴 Sin conexión" "offline" = "🔴 Desconectado"
"online" = "🟢 En línea" "online" = "🟢 En línea"
[tgbot.commands] [tgbot.commands]
@@ -646,7 +667,6 @@
"refreshedOn" = "\r\n📋🔄 Actualizado en: {{ .Time }}\r\n\r\n" "refreshedOn" = "\r\n📋🔄 Actualizado en: {{ .Time }}\r\n\r\n"
"yes" = "✅ Sí" "yes" = "✅ Sí"
"no" = "❌ No" "no" = "❌ No"
"received_id" = "🔑📥 ID actualizado." "received_id" = "🔑📥 ID actualizado."
"received_password" = "🔑📥 Contraseña actualizada." "received_password" = "🔑📥 Contraseña actualizada."
"received_email" = "📧📥 Correo electrónico actualizado." "received_email" = "📧📥 Correo electrónico actualizado."
@@ -666,7 +686,6 @@
"FailedResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ❌ Fallido \n\n🛠 Error: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ❌ Fallido \n\n🛠 Error: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Proceso de reinicio de tráfico finalizado para todos los clientes." "FinishProcess" = "🔚 Proceso de reinicio de tráfico finalizado para todos los clientes."
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ Cerrar Teclado" "closeKeyboard" = "❌ Cerrar Teclado"
"cancel" = "❌ Cancelar" "cancel" = "❌ Cancelar"
@@ -700,7 +719,6 @@
"limitTraffic" = "🚧 Límite de tráfico" "limitTraffic" = "🚧 Límite de tráfico"
"getBanLogs" = "Registros de prohibición" "getBanLogs" = "Registros de prohibición"
"allClients" = "Todos los Clientes" "allClients" = "Todos los Clientes"
"addClient" = "Añadir cliente" "addClient" = "Añadir cliente"
"submitDisable" = "Enviar como deshabilitado ☑️" "submitDisable" = "Enviar como deshabilitado ☑️"
"submitEnable" = "Enviar como habilitado ✅" "submitEnable" = "Enviar como habilitado ✅"
@@ -712,7 +730,6 @@
"ResetAllTraffics" = "Reiniciar todo el tráfico" "ResetAllTraffics" = "Reiniciar todo el tráfico"
"SortedTrafficUsageReport" = "Informe de uso de tráfico ordenado" "SortedTrafficUsageReport" = "Informe de uso de tráfico ordenado"
[tgbot.answers] [tgbot.answers]
"successfulOperation" = "✅ ¡Exitosa!" "successfulOperation" = "✅ ¡Exitosa!"
"errorOperation" = "❗ Error en la Operación." "errorOperation" = "❗ Error en la Operación."

View File

@@ -50,6 +50,7 @@
"fail" = "ناموفق" "fail" = "ناموفق"
"comment" = "توضیحات" "comment" = "توضیحات"
"success" = "موفق" "success" = "موفق"
"lastOnline" = "آخرین فعالیت"
"getVersion" = "دریافت نسخه" "getVersion" = "دریافت نسخه"
"install" = "نصب" "install" = "نصب"
"clients" = "کاربران" "clients" = "کاربران"
@@ -71,6 +72,20 @@
"emptyReverseDesc" = "هیچ پروکسی معکوس اضافه نشده است." "emptyReverseDesc" = "هیچ پروکسی معکوس اضافه نشده است."
"somethingWentWrong" = "مشکلی پیش آمد" "somethingWentWrong" = "مشکلی پیش آمد"
[subscription]
"title" = "اطلاعات سابسکریپشن"
"subId" = "شناسه اشتراک"
"status" = "وضعیت"
"downloaded" = "دانلود"
"uploaded" = "آپلود"
"expiry" = "تاریخ پایان"
"totalQuota" = "حجم کلی"
"individualLinks" = "لینک‌های تکی"
"active" = "فعال"
"inactive" = "غیرفعال"
"unlimited" = "نامحدود"
"noExpiry" = "بدون انقضا"
[menu] [menu]
"theme" = "تم" "theme" = "تم"
"dark" = "تیره" "dark" = "تیره"
@@ -117,8 +132,6 @@
"operationHours" = "مدت‌کارکرد" "operationHours" = "مدت‌کارکرد"
"systemLoad" = "بارسیستم" "systemLoad" = "بارسیستم"
"systemLoadDesc" = "میانگین بار سیستم برای 1، 5 و 15 دقیقه گذشته" "systemLoadDesc" = "میانگین بار سیستم برای 1، 5 و 15 دقیقه گذشته"
"connectionTcpCountDesc" = "در تمام‌شبکه‌ها TCP مجموع‌اتصالات"
"connectionUdpCountDesc" = "در تمام‌شبکه‌ها UDP مجموع‌اتصالات"
"connectionCount" = "تعداد کانکشن ها" "connectionCount" = "تعداد کانکشن ها"
"ipAddresses" = "آدرس‌های IP" "ipAddresses" = "آدرس‌های IP"
"toggleIpVisibility" = "تغییر وضعیت نمایش IP" "toggleIpVisibility" = "تغییر وضعیت نمایش IP"
@@ -134,6 +147,8 @@
"xraySwitchVersionPopover" = "Xray با موفقیت به‌روز شد" "xraySwitchVersionPopover" = "Xray با موفقیت به‌روز شد"
"geofileUpdateDialog" = "آیا واقعاً می‌خواهید فایل جغرافیایی را به‌روز کنید؟" "geofileUpdateDialog" = "آیا واقعاً می‌خواهید فایل جغرافیایی را به‌روز کنید؟"
"geofileUpdateDialogDesc" = "این عمل فایل #filename# را به‌روز می‌کند." "geofileUpdateDialogDesc" = "این عمل فایل #filename# را به‌روز می‌کند."
"geofilesUpdateDialogDesc" = "با این کار همه فایل‌ها به‌روزرسانی می‌شوند."
"geofilesUpdateAll" = "همه را به‌روزرسانی کنید"
"geofileUpdatePopover" = "فایل جغرافیایی با موفقیت به‌روز شد" "geofileUpdatePopover" = "فایل جغرافیایی با موفقیت به‌روز شد"
"dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید" "dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید"
"logs" = "گزارش‌ها" "logs" = "گزارش‌ها"
@@ -151,6 +166,8 @@
"getConfigError" = "خطا در دریافت فایل پیکربندی" "getConfigError" = "خطا در دریافت فایل پیکربندی"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "کل ترافیک"
"allTimeTrafficUsage" = "کل استفاده در تمام مدت"
"title" = "کاربران" "title" = "کاربران"
"totalDownUp" = "دریافت/ارسال کل" "totalDownUp" = "دریافت/ارسال کل"
"totalUsage" = "‌‌‌مصرف کل" "totalUsage" = "‌‌‌مصرف کل"
@@ -160,10 +177,13 @@
"remark" = "نام" "remark" = "نام"
"protocol" = "پروتکل" "protocol" = "پروتکل"
"port" = "پورت" "port" = "پورت"
"portMap" = "پورت‌های نظیر"
"traffic" = "ترافیک" "traffic" = "ترافیک"
"details" = "توضیحات" "details" = "توضیحات"
"transportConfig" = "نحوه اتصال" "transportConfig" = "نحوه اتصال"
"expireDate" = "مدت زمان" "expireDate" = "مدت زمان"
"createdAt" = "ایجاد"
"updatedAt" = "به‌روزرسانی"
"resetTraffic" = "ریست ترافیک" "resetTraffic" = "ریست ترافیک"
"addInbound" = "افزودن ورودی" "addInbound" = "افزودن ورودی"
"generalActions" = "عملیات کلی" "generalActions" = "عملیات کلی"
@@ -175,8 +195,6 @@
"deleteClient" = "حذف کاربر" "deleteClient" = "حذف کاربر"
"deleteClientContent" = "آیا مطمئن به حذف کاربر هستید؟" "deleteClientContent" = "آیا مطمئن به حذف کاربر هستید؟"
"resetTrafficContent" = "آیا مطمئن به ریست ترافیک هستید؟" "resetTrafficContent" = "آیا مطمئن به ریست ترافیک هستید؟"
"inboundUpdateSuccess" = "ورودی با موفقیت به‌روزرسانی شد."
"inboundCreateSuccess" = "ورودی با موفقیت ایجاد شد."
"copyLink" = "کپی لینک" "copyLink" = "کپی لینک"
"address" = "آدرس" "address" = "آدرس"
"network" = "شبکه" "network" = "شبکه"
@@ -262,6 +280,8 @@
"resetInboundClientTrafficSuccess" = "ترافیک بازنشانی شد" "resetInboundClientTrafficSuccess" = "ترافیک بازنشانی شد"
"trafficGetError" = "خطا در دریافت ترافیک‌ها" "trafficGetError" = "خطا در دریافت ترافیک‌ها"
"getNewX25519CertError" = "خطا در دریافت گواهی X25519." "getNewX25519CertError" = "خطا در دریافت گواهی X25519."
"getNewmldsa65Error" = "خطا در دریافت گواهی mldsa65."
"getNewVlessEncError" = "خطا در دریافت گواهی VlessEnc."
[pages.inbounds.stream.general] [pages.inbounds.stream.general]
"request" = "درخواست" "request" = "درخواست"
@@ -358,16 +378,16 @@
"subDomainDesc" = "آدرس دامنه برای سرویس سابسکریپشن. برای گوش دادن به تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید‌" "subDomainDesc" = "آدرس دامنه برای سرویس سابسکریپشن. برای گوش دادن به تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید‌"
"subUpdates" = "فاصله بروزرسانی‌ سابسکریپشن" "subUpdates" = "فاصله بروزرسانی‌ سابسکریپشن"
"subUpdatesDesc" = "(فاصله مابین بروزرسانی در برنامه‌های کاربری. (واحد: ساعت" "subUpdatesDesc" = "(فاصله مابین بروزرسانی در برنامه‌های کاربری. (واحد: ساعت"
"externalTrafficInformEnable" = "اطلاع رسانی خارجی مصرف ترافیک"
"externalTrafficInformEnableDesc" = "مصرف ترافیک به سرویس خارجی ارسال می شود"
"externalTrafficInformURI" = "لینک اطلاع رسانی خارجی مصرف ترافیک"
"externalTrafficInformURIDesc" = "ترافیک های مصرفی به این لینک هم ارسال می شود"
"subEncrypt" = "کدگذاری" "subEncrypt" = "کدگذاری"
"subEncryptDesc" = "کدگذاری خواهدشد Base64 محتوای برگشتی سرویس سابسکریپشن برپایه" "subEncryptDesc" = "کدگذاری خواهدشد Base64 محتوای برگشتی سرویس سابسکریپشن برپایه"
"subShowInfo" = "نمایش اطلاعات مصرف" "subShowInfo" = "نمایش اطلاعات مصرف"
"subShowInfoDesc" = "ترافیک و زمان باقی‌مانده را در برنامه‌های کاربری نمایش می‌دهد" "subShowInfoDesc" = "ترافیک و زمان باقی‌مانده را در برنامه‌های کاربری نمایش می‌دهد"
"subURI" = "پروکسی معکوس URI مسیر" "subURI" = "پروکسی معکوس URI مسیر"
"subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر" "subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر"
"externalTrafficInformEnable" = "اطلاع رسانی خارجی مصرف ترافیک"
"externalTrafficInformEnableDesc" = "مصرف ترافیک به سرویس خارجی ارسال می شود"
"externalTrafficInformURI" = "لینک اطلاع رسانی خارجی مصرف ترافیک"
"externalTrafficInformURIDesc" = "ترافیک های مصرفی به این لینک هم ارسال می شود"
"fragment" = "فرگمنت" "fragment" = "فرگمنت"
"fragmentDesc" = "فعال کردن فرگمنت برای بسته‌ی نخست تی‌ال‌اس" "fragmentDesc" = "فعال کردن فرگمنت برای بسته‌ی نخست تی‌ال‌اس"
"fragmentSett" = "تنظیمات فرگمنت" "fragmentSett" = "تنظیمات فرگمنت"
@@ -561,22 +581,23 @@
"resetOutboundTrafficError" = "خطا در بازنشانی ترافیک خروجی" "resetOutboundTrafficError" = "خطا در بازنشانی ترافیک خروجی"
[tgbot] [tgbot]
"keyboardClosed" = "❌ کیبورد سفارشی بسته شد!" "keyboardClosed" = "❌ صفحه کلید بسته شد!"
"noResult" = "❗ نتیجهای یافت نشد!" "noResult" = "❗ نتیجه ای یافت نشد!"
"noQuery" = "❌ کوئری یافت نشد! لطفاً دستور را مجدداً استفاده کنید!" "noQuery" = "❌ درخواست یافت نشد! لطفا دوباره تلاش کنید!"
"wentWrong" = "❌ مشکلی رخ داده است!" "wentWrong" = "❌ مشکلی پیش آمد!"
"noIpRecord" = "❗ رکورد IP یافت نشد!" "noIpRecord" = "❗ رکورد آی پی وجود ندارد!"
"noInbounds" = "❗ هیچ ورودی یافت نشد!" "noInbounds" = "❗ هیچ ورودی یافت نشد!"
"unlimited" = "♾ - نامحدود(ریست)" "unlimited" = "♾ نامحدود(ریست)"
"add" = "اضافه کردن" "add" = "افزودن"
"month" = "ماه" "month" = "ماه"
"months" = "ماه" "months" = "ماه"
"day" = "روز" "day" = "روز"
"days" = "روز" "days" = "روز"
"hours" = "ساعت" "hours" = "ساعت"
"minutes" = "دقیقه"
"unknown" = "نامشخص" "unknown" = "نامشخص"
"inbounds" = "ورودیها" "inbounds" = "ورودی ها"
"clients" = لاینت‌ها" "clients" = اربران"
"offline" = "🔴 آفلاین" "offline" = "🔴 آفلاین"
"online" = "🟢 آنلاین" "online" = "🟢 آنلاین"
@@ -646,7 +667,6 @@
"refreshedOn" = "\r\n📋🔄 تازه‌سازی شده در: {{ .Time }}\r\n\r\n" "refreshedOn" = "\r\n📋🔄 تازه‌سازی شده در: {{ .Time }}\r\n\r\n"
"yes" = "✅ بله" "yes" = "✅ بله"
"no" = "❌ خیر" "no" = "❌ خیر"
"received_id" = "🔑📥 شناسه به‌روزرسانی شد." "received_id" = "🔑📥 شناسه به‌روزرسانی شد."
"received_password" = "🔑📥 رمز عبور به‌روزرسانی شد." "received_password" = "🔑📥 رمز عبور به‌روزرسانی شد."
"received_email" = "📧📥 ایمیل به‌روزرسانی شد." "received_email" = "📧📥 ایمیل به‌روزرسانی شد."
@@ -666,7 +686,6 @@
"FailedResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ❌ ناموفق \n\n🛠 خطا: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ❌ ناموفق \n\n🛠 خطا: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 فرآیند بازنشانی ترافیک برای همه مشتریان به پایان رسید." "FinishProcess" = "🔚 فرآیند بازنشانی ترافیک برای همه مشتریان به پایان رسید."
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ بستن کیبورد" "closeKeyboard" = "❌ بستن کیبورد"
"cancel" = "❌ لغو" "cancel" = "❌ لغو"
@@ -700,7 +719,6 @@
"limitTraffic" = "🚧 محدودیت ترافیک" "limitTraffic" = "🚧 محدودیت ترافیک"
"getBanLogs" = "گزارش های بلوک را دریافت کنید" "getBanLogs" = "گزارش های بلوک را دریافت کنید"
"allClients" = "همه مشتریان" "allClients" = "همه مشتریان"
"addClient" = "افزودن مشتری" "addClient" = "افزودن مشتری"
"submitDisable" = "ارسال به عنوان غیرفعال ☑️" "submitDisable" = "ارسال به عنوان غیرفعال ☑️"
"submitEnable" = "ارسال به عنوان فعال ✅" "submitEnable" = "ارسال به عنوان فعال ✅"
@@ -712,7 +730,6 @@
"ResetAllTraffics" = "بازنشانی همه ترافیک‌ها" "ResetAllTraffics" = "بازنشانی همه ترافیک‌ها"
"SortedTrafficUsageReport" = "گزارش استفاده از ترافیک مرتب‌شده" "SortedTrafficUsageReport" = "گزارش استفاده از ترافیک مرتب‌شده"
[tgbot.answers] [tgbot.answers]
"successfulOperation" = "✅ انجام شد!" "successfulOperation" = "✅ انجام شد!"
"errorOperation" = "❗ خطا در عملیات." "errorOperation" = "❗ خطا در عملیات."

View File

@@ -50,6 +50,7 @@
"fail" = "Gagal" "fail" = "Gagal"
"comment" = "Komentar" "comment" = "Komentar"
"success" = "Berhasil" "success" = "Berhasil"
"lastOnline" = "Terakhir online"
"getVersion" = "Dapatkan Versi" "getVersion" = "Dapatkan Versi"
"install" = "Instal" "install" = "Instal"
"clients" = "Klien" "clients" = "Klien"
@@ -71,6 +72,20 @@
"emptyReverseDesc" = "Tidak ada proxy terbalik yang ditambahkan." "emptyReverseDesc" = "Tidak ada proxy terbalik yang ditambahkan."
"somethingWentWrong" = "Terjadi kesalahan" "somethingWentWrong" = "Terjadi kesalahan"
[subscription]
"title" = "Info langganan"
"subId" = "ID langganan"
"status" = "Status"
"downloaded" = "Diunduh"
"uploaded" = "Diunggah"
"expiry" = "Kedaluwarsa"
"totalQuota" = "Kuota total"
"individualLinks" = "Tautan individual"
"active" = "Aktif"
"inactive" = "Nonaktif"
"unlimited" = "Tanpa batas"
"noExpiry" = "Tanpa kedaluwarsa"
[menu] [menu]
"theme" = "Tema" "theme" = "Tema"
"dark" = "Gelap" "dark" = "Gelap"
@@ -117,8 +132,6 @@
"operationHours" = "Waktu Aktif" "operationHours" = "Waktu Aktif"
"systemLoad" = "Beban Sistem" "systemLoad" = "Beban Sistem"
"systemLoadDesc" = "Rata-rata beban sistem selama 1, 5, dan 15 menit terakhir" "systemLoadDesc" = "Rata-rata beban sistem selama 1, 5, dan 15 menit terakhir"
"connectionTcpCountDesc" = "Total koneksi TCP di seluruh sistem"
"connectionUdpCountDesc" = "Total koneksi UDP di seluruh sistem"
"connectionCount" = "Statistik Koneksi" "connectionCount" = "Statistik Koneksi"
"ipAddresses" = "Alamat IP" "ipAddresses" = "Alamat IP"
"toggleIpVisibility" = "Alihkan visibilitas IP" "toggleIpVisibility" = "Alihkan visibilitas IP"
@@ -134,6 +147,8 @@
"xraySwitchVersionPopover" = "Xray berhasil diperbarui" "xraySwitchVersionPopover" = "Xray berhasil diperbarui"
"geofileUpdateDialog" = "Apakah Anda yakin ingin memperbarui geofile?" "geofileUpdateDialog" = "Apakah Anda yakin ingin memperbarui geofile?"
"geofileUpdateDialogDesc" = "Ini akan memperbarui file #filename#." "geofileUpdateDialogDesc" = "Ini akan memperbarui file #filename#."
"geofilesUpdateDialogDesc" = "Ini akan memperbarui semua berkas."
"geofilesUpdateAll" = "Perbarui semua"
"geofileUpdatePopover" = "Geofile berhasil diperbarui" "geofileUpdatePopover" = "Geofile berhasil diperbarui"
"dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini" "dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini"
"logs" = "Log" "logs" = "Log"
@@ -151,6 +166,8 @@
"getConfigError" = "Terjadi kesalahan saat mengambil file konfigurasi" "getConfigError" = "Terjadi kesalahan saat mengambil file konfigurasi"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Total Lalu Lintas"
"allTimeTrafficUsage" = "Total Penggunaan Sepanjang Waktu"
"title" = "Masuk" "title" = "Masuk"
"totalDownUp" = "Total Terkirim/Diterima" "totalDownUp" = "Total Terkirim/Diterima"
"totalUsage" = "Penggunaan Total" "totalUsage" = "Penggunaan Total"
@@ -160,10 +177,13 @@
"remark" = "Catatan" "remark" = "Catatan"
"protocol" = "Protokol" "protocol" = "Protokol"
"port" = "Port" "port" = "Port"
"portMap" = "Port Mapping"
"traffic" = "Traffic" "traffic" = "Traffic"
"details" = "Rincian" "details" = "Rincian"
"transportConfig" = "Transport" "transportConfig" = "Transport"
"expireDate" = "Durasi" "expireDate" = "Durasi"
"createdAt" = "Dibuat"
"updatedAt" = "Diperbarui"
"resetTraffic" = "Reset Traffic" "resetTraffic" = "Reset Traffic"
"addInbound" = "Tambahkan Masuk" "addInbound" = "Tambahkan Masuk"
"generalActions" = "Tindakan Umum" "generalActions" = "Tindakan Umum"
@@ -175,8 +195,6 @@
"deleteClient" = "Hapus Klien" "deleteClient" = "Hapus Klien"
"deleteClientContent" = "Apakah Anda yakin ingin menghapus klien?" "deleteClientContent" = "Apakah Anda yakin ingin menghapus klien?"
"resetTrafficContent" = "Apakah Anda yakin ingin mereset traffic?" "resetTrafficContent" = "Apakah Anda yakin ingin mereset traffic?"
"inboundUpdateSuccess" = "Inbound berhasil diperbarui."
"inboundCreateSuccess" = "Inbound berhasil dibuat."
"copyLink" = "Salin URL" "copyLink" = "Salin URL"
"address" = "Alamat" "address" = "Alamat"
"network" = "Jaringan" "network" = "Jaringan"
@@ -262,6 +280,8 @@
"resetInboundClientTrafficSuccess" = "Lalu lintas telah direset" "resetInboundClientTrafficSuccess" = "Lalu lintas telah direset"
"trafficGetError" = "Gagal mendapatkan data lalu lintas" "trafficGetError" = "Gagal mendapatkan data lalu lintas"
"getNewX25519CertError" = "Terjadi kesalahan saat mendapatkan sertifikat X25519." "getNewX25519CertError" = "Terjadi kesalahan saat mendapatkan sertifikat X25519."
"getNewmldsa65Error" = "Terjadi kesalahan saat mendapatkan sertifikat mldsa65."
"getNewVlessEncError" = "Terjadi kesalahan saat mendapatkan sertifikat VlessEnc."
[pages.inbounds.stream.general] [pages.inbounds.stream.general]
"request" = "Permintaan" "request" = "Permintaan"
@@ -421,7 +441,6 @@
"RoutingStrategy" = "Strategi Pengalihan Keseluruhan" "RoutingStrategy" = "Strategi Pengalihan Keseluruhan"
"RoutingStrategyDesc" = "Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan." "RoutingStrategyDesc" = "Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan."
"Torrent" = "Blokir Protokol BitTorrent" "Torrent" = "Blokir Protokol BitTorrent"
"TorrentDesc" = "Memblokir protokol BitTorrent."
"Inbounds" = "Masuk" "Inbounds" = "Masuk"
"InboundsDesc" = "Menerima klien tertentu." "InboundsDesc" = "Menerima klien tertentu."
"Outbounds" = "Keluar" "Outbounds" = "Keluar"
@@ -562,21 +581,22 @@
"resetOutboundTrafficError" = "Gagal mereset lalu lintas keluar" "resetOutboundTrafficError" = "Gagal mereset lalu lintas keluar"
[tgbot] [tgbot]
"keyboardClosed" = "❌ Papan ketik kustom ditutup!" "keyboardClosed" = "❌ Keyboard ditutup!"
"noResult" = "❗ Tidak ada hasil!" "noResult" = "❗ Tidak ada hasil!"
"noQuery" = "❌ Permintaan tidak ditemukan! Harap gunakan perintah lagi!" "noQuery" = "❌ Kueri tidak ditemukan! Silakan gunakan perintah lagi!"
"wentWrong" = "❌ Ada yang salah!" "wentWrong" = "❌ Terjadi kesalahan!"
"noIpRecord" = "❗ Tidak ada Catatan IP!" "noIpRecord" = "❗ Tidak ada Catatan IP!"
"noInbounds" = "❗ Tidak ada masuk ditemukan!" "noInbounds" = "❗ Tidak ada inbound yang ditemukan!"
"unlimited" = "♾ Tak terbatas" "unlimited" = "♾ Tidak terbatas (Reset)"
"add" = "Tambah" "add" = "Tambah"
"month" = "Bulan" "month" = "Bulan"
"months" = "Bulan" "months" = "Bulan"
"day" = "Hari" "day" = "Hari"
"days" = "Hari" "days" = "Hari"
"hours" = "Jam" "hours" = "Jam"
"minutes" = "Menit"
"unknown" = "Tidak diketahui" "unknown" = "Tidak diketahui"
"inbounds" = "Masuk" "inbounds" = "Inbound"
"clients" = "Klien" "clients" = "Klien"
"offline" = "🔴 Offline" "offline" = "🔴 Offline"
"online" = "🟢 Online" "online" = "🟢 Online"
@@ -647,7 +667,6 @@
"refreshedOn" = "\r\n📋🔄 Diperbarui Pada: {{ .Time }}\r\n\r\n" "refreshedOn" = "\r\n📋🔄 Diperbarui Pada: {{ .Time }}\r\n\r\n"
"yes" = "✅ Ya" "yes" = "✅ Ya"
"no" = "❌ Tidak" "no" = "❌ Tidak"
"received_id" = "🔑📥 ID diperbarui." "received_id" = "🔑📥 ID diperbarui."
"received_password" = "🔑📥 Kata sandi diperbarui." "received_password" = "🔑📥 Kata sandi diperbarui."
"received_email" = "📧📥 Email diperbarui." "received_email" = "📧📥 Email diperbarui."
@@ -667,7 +686,6 @@
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ❌ Gagal \n\n🛠 Kesalahan: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ❌ Gagal \n\n🛠 Kesalahan: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Proses reset traffic selesai untuk semua klien." "FinishProcess" = "🔚 Proses reset traffic selesai untuk semua klien."
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ Tutup Papan Ketik" "closeKeyboard" = "❌ Tutup Papan Ketik"
"cancel" = "❌ Batal" "cancel" = "❌ Batal"
@@ -701,7 +719,6 @@
"limitTraffic" = "🚧 Batas Lalu Lintas" "limitTraffic" = "🚧 Batas Lalu Lintas"
"getBanLogs" = "Dapatkan Log Pemblokiran" "getBanLogs" = "Dapatkan Log Pemblokiran"
"allClients" = "Semua Klien" "allClients" = "Semua Klien"
"addClient" = "Tambah Klien" "addClient" = "Tambah Klien"
"submitDisable" = "Kirim Sebagai Nonaktif ☑️" "submitDisable" = "Kirim Sebagai Nonaktif ☑️"
"submitEnable" = "Kirim Sebagai Aktif ✅" "submitEnable" = "Kirim Sebagai Aktif ✅"
@@ -713,7 +730,6 @@
"ResetAllTraffics" = "Reset Semua Lalu Lintas" "ResetAllTraffics" = "Reset Semua Lalu Lintas"
"SortedTrafficUsageReport" = "Laporan Penggunaan Lalu Lintas yang Terurut" "SortedTrafficUsageReport" = "Laporan Penggunaan Lalu Lintas yang Terurut"
[tgbot.answers] [tgbot.answers]
"successfulOperation" = "✅ Operasi berhasil!" "successfulOperation" = "✅ Operasi berhasil!"
"errorOperation" = "❗ Kesalahan dalam operasi." "errorOperation" = "❗ Kesalahan dalam operasi."

View File

@@ -50,6 +50,7 @@
"fail" = "失敗" "fail" = "失敗"
"comment" = "コメント" "comment" = "コメント"
"success" = "成功" "success" = "成功"
"lastOnline" = "最終オンライン"
"getVersion" = "バージョン取得" "getVersion" = "バージョン取得"
"install" = "インストール" "install" = "インストール"
"clients" = "クライアント" "clients" = "クライアント"
@@ -71,6 +72,20 @@
"emptyReverseDesc" = "追加されたリバースプロキシはありません。" "emptyReverseDesc" = "追加されたリバースプロキシはありません。"
"somethingWentWrong" = "エラーが発生しました" "somethingWentWrong" = "エラーが発生しました"
[subscription]
"title" = "サブスクリプション情報"
"subId" = "サブスクリプションID"
"status" = "ステータス"
"downloaded" = "ダウンロード"
"uploaded" = "アップロード"
"expiry" = "有効期限"
"totalQuota" = "合計クォータ"
"individualLinks" = "個別リンク"
"active" = "有効"
"inactive" = "無効"
"unlimited" = "無制限"
"noExpiry" = "期限なし"
[menu] [menu]
"theme" = "テーマ" "theme" = "テーマ"
"dark" = "ダーク" "dark" = "ダーク"
@@ -117,8 +132,6 @@
"operationHours" = "システム稼働時間" "operationHours" = "システム稼働時間"
"systemLoad" = "システム負荷" "systemLoad" = "システム負荷"
"systemLoadDesc" = "過去1、5、15分間のシステム平均負荷" "systemLoadDesc" = "過去1、5、15分間のシステム平均負荷"
"connectionTcpCountDesc" = "システム内のすべてのTCP接続数"
"connectionUdpCountDesc" = "システム内のすべてのUDP接続数"
"connectionCount" = "接続数" "connectionCount" = "接続数"
"ipAddresses" = "IPアドレス" "ipAddresses" = "IPアドレス"
"toggleIpVisibility" = "IPの表示を切り替える" "toggleIpVisibility" = "IPの表示を切り替える"
@@ -134,6 +147,8 @@
"xraySwitchVersionPopover" = "Xrayの更新が成功しました" "xraySwitchVersionPopover" = "Xrayの更新が成功しました"
"geofileUpdateDialog" = "ジオファイルを本当に更新しますか?" "geofileUpdateDialog" = "ジオファイルを本当に更新しますか?"
"geofileUpdateDialogDesc" = "これにより#filename#ファイルが更新されます。" "geofileUpdateDialogDesc" = "これにより#filename#ファイルが更新されます。"
"geofilesUpdateDialogDesc" = "これにより、すべてのファイルが更新されます。"
"geofilesUpdateAll" = "すべて更新"
"geofileUpdatePopover" = "ジオファイルの更新が成功しました" "geofileUpdatePopover" = "ジオファイルの更新が成功しました"
"dontRefresh" = "インストール中、このページをリロードしないでください" "dontRefresh" = "インストール中、このページをリロードしないでください"
"logs" = "ログ" "logs" = "ログ"
@@ -151,6 +166,8 @@
"getConfigError" = "設定ファイルの取得中にエラーが発生しました" "getConfigError" = "設定ファイルの取得中にエラーが発生しました"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "総トラフィック"
"allTimeTrafficUsage" = "これまでの総使用量"
"title" = "インバウンド一覧" "title" = "インバウンド一覧"
"totalDownUp" = "総アップロード / ダウンロード" "totalDownUp" = "総アップロード / ダウンロード"
"totalUsage" = "総使用量" "totalUsage" = "総使用量"
@@ -160,10 +177,13 @@
"remark" = "備考" "remark" = "備考"
"protocol" = "プロトコル" "protocol" = "プロトコル"
"port" = "ポート" "port" = "ポート"
"portMap" = "ポートマッピング"
"traffic" = "トラフィック" "traffic" = "トラフィック"
"details" = "詳細情報" "details" = "詳細情報"
"transportConfig" = "トランスポート設定" "transportConfig" = "トランスポート設定"
"expireDate" = "有効期限" "expireDate" = "有効期限"
"createdAt" = "作成"
"updatedAt" = "更新"
"resetTraffic" = "トラフィックリセット" "resetTraffic" = "トラフィックリセット"
"addInbound" = "インバウンド追加" "addInbound" = "インバウンド追加"
"generalActions" = "一般操作" "generalActions" = "一般操作"
@@ -175,8 +195,6 @@
"deleteClient" = "クライアント削除" "deleteClient" = "クライアント削除"
"deleteClientContent" = "クライアントを削除してもよろしいですか?" "deleteClientContent" = "クライアントを削除してもよろしいですか?"
"resetTrafficContent" = "トラフィックをリセットしてもよろしいですか?" "resetTrafficContent" = "トラフィックをリセットしてもよろしいですか?"
"inboundUpdateSuccess" = "インバウンドが正常に更新されました。"
"inboundCreateSuccess" = "インバウンドが正常に作成されました。"
"copyLink" = "リンクをコピー" "copyLink" = "リンクをコピー"
"address" = "アドレス" "address" = "アドレス"
"network" = "ネットワーク" "network" = "ネットワーク"
@@ -262,6 +280,8 @@
"resetInboundClientTrafficSuccess" = "トラフィックがリセットされました" "resetInboundClientTrafficSuccess" = "トラフィックがリセットされました"
"trafficGetError" = "トラフィックの取得中にエラーが発生しました" "trafficGetError" = "トラフィックの取得中にエラーが発生しました"
"getNewX25519CertError" = "X25519証明書の取得中にエラーが発生しました。" "getNewX25519CertError" = "X25519証明書の取得中にエラーが発生しました。"
"getNewmldsa65Error" = "mldsa65証明書の取得中にエラーが発生しました。"
"getNewVlessEncError" = "VlessEnc証明書の取得中にエラーが発生しました。"
[pages.inbounds.stream.general] [pages.inbounds.stream.general]
"request" = "リクエスト" "request" = "リクエスト"
@@ -561,21 +581,22 @@
"resetOutboundTrafficError" = "送信トラフィックのリセットエラー" "resetOutboundTrafficError" = "送信トラフィックのリセットエラー"
[tgbot] [tgbot]
"keyboardClosed" = "❌ カスタムキーボード閉じられました!" "keyboardClosed" = "❌ キーボード閉じました!"
"noResult" = "❗ 結果がありません!" "noResult" = "❗ 結果がありません!"
"noQuery" = "❌ クエリが見つかりませんでした!もう一度コマンドを使用してください!" "noQuery" = "❌ クエリが見つかりませんコマンドを再利用してください!"
"wentWrong" = "❌ 問題が発生しました!" "wentWrong" = "❌ 何かがうまくいかなかった!"
"noIpRecord" = "❗ IP記録がありません!" "noIpRecord" = "❗ IPレコードがありません!"
"noInbounds" = "❗ インバウンド接続が見つかりません!" "noInbounds" = "❗ インバウンドが見つかりません!"
"unlimited" = "♾ 無制限" "unlimited" = "♾ 無制限(リセット)"
"add" = "追加" "add" = "追加"
"month" = "月" "month" = "月"
"months" = "月" "months" = "月"
"day" = "日" "day" = "日"
"days" = "日" "days" = "日"
"hours" = "時間" "hours" = "時間"
"minutes" = "分"
"unknown" = "不明" "unknown" = "不明"
"inbounds" = "インバウンド接続" "inbounds" = "インバウンド"
"clients" = "クライアント" "clients" = "クライアント"
"offline" = "🔴 オフライン" "offline" = "🔴 オフライン"
"online" = "🟢 オンライン" "online" = "🟢 オンライン"
@@ -646,7 +667,6 @@
"refreshedOn" = "\r\n📋🔄 更新時間:{{ .Time }}\r\n\r\n" "refreshedOn" = "\r\n📋🔄 更新時間:{{ .Time }}\r\n\r\n"
"yes" = "✅ はい" "yes" = "✅ はい"
"no" = "❌ いいえ" "no" = "❌ いいえ"
"received_id" = "🔑📥 IDが更新されました。" "received_id" = "🔑📥 IDが更新されました。"
"received_password" = "🔑📥 パスワードが更新されました。" "received_password" = "🔑📥 パスワードが更新されました。"
"received_email" = "📧📥 メールが更新されました。" "received_email" = "📧📥 メールが更新されました。"
@@ -666,7 +686,6 @@
"FailedResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠 エラー: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠 エラー: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 すべてのクライアントのトラフィックリセットが完了しました。" "FinishProcess" = "🔚 すべてのクライアントのトラフィックリセットが完了しました。"
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ キーボードを閉じる" "closeKeyboard" = "❌ キーボードを閉じる"
"cancel" = "❌ キャンセル" "cancel" = "❌ キャンセル"
@@ -700,7 +719,6 @@
"limitTraffic" = "🚧 トラフィック制限" "limitTraffic" = "🚧 トラフィック制限"
"getBanLogs" = "禁止ログ" "getBanLogs" = "禁止ログ"
"allClients" = "すべてのクライアント" "allClients" = "すべてのクライアント"
"addClient" = "クライアントを追加" "addClient" = "クライアントを追加"
"submitDisable" = "無効として送信 ☑️" "submitDisable" = "無効として送信 ☑️"
"submitEnable" = "有効として送信 ✅" "submitEnable" = "有効として送信 ✅"
@@ -712,7 +730,6 @@
"ResetAllTraffics" = "すべてのトラフィックをリセット" "ResetAllTraffics" = "すべてのトラフィックをリセット"
"SortedTrafficUsageReport" = "ソートされたトラフィック使用レポート" "SortedTrafficUsageReport" = "ソートされたトラフィック使用レポート"
[tgbot.answers] [tgbot.answers]
"successfulOperation" = "✅ 成功!" "successfulOperation" = "✅ 成功!"
"errorOperation" = "❗ 操作エラー。" "errorOperation" = "❗ 操作エラー。"

View File

@@ -50,6 +50,7 @@
"fail" = "Falhou" "fail" = "Falhou"
"comment" = "Comentário" "comment" = "Comentário"
"success" = "Com Sucesso" "success" = "Com Sucesso"
"lastOnline" = "Última vez online"
"getVersion" = "Obter Versão" "getVersion" = "Obter Versão"
"install" = "Instalar" "install" = "Instalar"
"clients" = "Clientes" "clients" = "Clientes"
@@ -71,6 +72,20 @@
"emptyReverseDesc" = "Nenhum proxy reverso adicionado." "emptyReverseDesc" = "Nenhum proxy reverso adicionado."
"somethingWentWrong" = "Algo deu errado" "somethingWentWrong" = "Algo deu errado"
[subscription]
"title" = "Informações da assinatura"
"subId" = "ID da assinatura"
"status" = "Status"
"downloaded" = "Baixado"
"uploaded" = "Enviado"
"expiry" = "Validade"
"totalQuota" = "Cota total"
"individualLinks" = "Links individuais"
"active" = "Ativo"
"inactive" = "Inativo"
"unlimited" = "Ilimitado"
"noExpiry" = "Sem validade"
[menu] [menu]
"theme" = "Tema" "theme" = "Tema"
"dark" = "Escuro" "dark" = "Escuro"
@@ -117,8 +132,6 @@
"operationHours" = "Tempo de Atividade" "operationHours" = "Tempo de Atividade"
"systemLoad" = "Carga do Sistema" "systemLoad" = "Carga do Sistema"
"systemLoadDesc" = "Média de carga do sistema nos últimos 1, 5 e 15 minutos" "systemLoadDesc" = "Média de carga do sistema nos últimos 1, 5 e 15 minutos"
"connectionTcpCountDesc" = "Total de conexões TCP no sistema"
"connectionUdpCountDesc" = "Total de conexões UDP no sistema"
"connectionCount" = "Estatísticas de Conexão" "connectionCount" = "Estatísticas de Conexão"
"ipAddresses" = "Endereços IP" "ipAddresses" = "Endereços IP"
"toggleIpVisibility" = "Alternar visibilidade do IP" "toggleIpVisibility" = "Alternar visibilidade do IP"
@@ -134,6 +147,8 @@
"xraySwitchVersionPopover" = "Xray atualizado com sucesso" "xraySwitchVersionPopover" = "Xray atualizado com sucesso"
"geofileUpdateDialog" = "Você realmente deseja atualizar o geofile?" "geofileUpdateDialog" = "Você realmente deseja atualizar o geofile?"
"geofileUpdateDialogDesc" = "Isso atualizará o arquivo #filename#." "geofileUpdateDialogDesc" = "Isso atualizará o arquivo #filename#."
"geofilesUpdateDialogDesc" = "Isso atualizará todos os arquivos."
"geofilesUpdateAll" = "Atualizar tudo"
"geofileUpdatePopover" = "Geofile atualizado com sucesso" "geofileUpdatePopover" = "Geofile atualizado com sucesso"
"dontRefresh" = "Instalação em andamento, por favor não atualize a página" "dontRefresh" = "Instalação em andamento, por favor não atualize a página"
"logs" = "Logs" "logs" = "Logs"
@@ -151,6 +166,8 @@
"getConfigError" = "Ocorreu um erro ao recuperar o arquivo de configuração" "getConfigError" = "Ocorreu um erro ao recuperar o arquivo de configuração"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Tráfego Total"
"allTimeTrafficUsage" = "Uso total de todos os tempos"
"title" = "Inbounds" "title" = "Inbounds"
"totalDownUp" = "Total Enviado/Recebido" "totalDownUp" = "Total Enviado/Recebido"
"totalUsage" = "Uso Total" "totalUsage" = "Uso Total"
@@ -160,10 +177,13 @@
"remark" = "Observação" "remark" = "Observação"
"protocol" = "Protocolo" "protocol" = "Protocolo"
"port" = "Porta" "port" = "Porta"
"portMap" = "Porta Mapeada"
"traffic" = "Tráfego" "traffic" = "Tráfego"
"details" = "Detalhes" "details" = "Detalhes"
"transportConfig" = "Transporte" "transportConfig" = "Transporte"
"expireDate" = "Duração" "expireDate" = "Duração"
"createdAt" = "Criado"
"updatedAt" = "Atualizado"
"resetTraffic" = "Redefinir Tráfego" "resetTraffic" = "Redefinir Tráfego"
"addInbound" = "Adicionar Inbound" "addInbound" = "Adicionar Inbound"
"generalActions" = "Ações Gerais" "generalActions" = "Ações Gerais"
@@ -175,8 +195,6 @@
"deleteClient" = "Excluir Cliente" "deleteClient" = "Excluir Cliente"
"deleteClientContent" = "Tem certeza de que deseja excluir o cliente?" "deleteClientContent" = "Tem certeza de que deseja excluir o cliente?"
"resetTrafficContent" = "Tem certeza de que deseja redefinir o tráfego?" "resetTrafficContent" = "Tem certeza de que deseja redefinir o tráfego?"
"inboundUpdateSuccess" = "A entrada foi atualizada com sucesso."
"inboundCreateSuccess" = "A entrada foi criada com sucesso."
"copyLink" = "Copiar URL" "copyLink" = "Copiar URL"
"address" = "Endereço" "address" = "Endereço"
"network" = "Rede" "network" = "Rede"
@@ -262,6 +280,8 @@
"resetInboundClientTrafficSuccess" = "O tráfego foi reiniciado" "resetInboundClientTrafficSuccess" = "O tráfego foi reiniciado"
"trafficGetError" = "Erro ao obter tráfegos" "trafficGetError" = "Erro ao obter tráfegos"
"getNewX25519CertError" = "Erro ao obter o certificado X25519." "getNewX25519CertError" = "Erro ao obter o certificado X25519."
"getNewmldsa65Error" = "Erro ao obter o certificado mldsa65."
"getNewVlessEncError" = "Erro ao obter o certificado VlessEnc."
[pages.inbounds.stream.general] [pages.inbounds.stream.general]
"request" = "Requisição" "request" = "Requisição"
@@ -561,21 +581,22 @@
"resetOutboundTrafficError" = "Erro ao redefinir tráfego de saída" "resetOutboundTrafficError" = "Erro ao redefinir tráfego de saída"
[tgbot] [tgbot]
"keyboardClosed" = "❌ Teclado personalizado fechado!" "keyboardClosed" = "❌ Teclado fechado!"
"noResult" = "❗ Nenhum resultado!" "noResult" = "❗ Nenhum resultado!"
"noQuery" = "❌ Consulta não encontrada! Por favor, use o comando novamente!" "noQuery" = "❌ Consulta não encontrada! Por favor, use o comando novamente!"
"wentWrong" = "❌ Algo deu errado!" "wentWrong" = "❌ Algo deu errado!"
"noIpRecord" = "❗ Nenhum registro de IP!" "noIpRecord" = "❗ Nenhum registro de IP!"
"noInbounds" = "❗ Nenhuma entrada encontrada!" "noInbounds" = "❗ Nenhum inbound encontrado!"
"unlimited" = "♾ Ilimitado (Reiniciar)" "unlimited" = "♾ Ilimitado (Reset)"
"add" = "Adicionar" "add" = "Adicionar"
"month" = "Mês" "month" = "Mês"
"months" = "Meses" "months" = "Meses"
"day" = "Dia" "day" = "Dia"
"days" = "Dias" "days" = "Dias"
"hours" = "Horas" "hours" = "Horas"
"minutes" = "Minutos"
"unknown" = "Desconhecido" "unknown" = "Desconhecido"
"inbounds" = "Entradas" "inbounds" = "Inbounds"
"clients" = "Clientes" "clients" = "Clientes"
"offline" = "🔴 Offline" "offline" = "🔴 Offline"
"online" = "🟢 Online" "online" = "🟢 Online"
@@ -646,7 +667,6 @@
"refreshedOn" = "\r\n📋🔄 Atualizado em: {{ .Time }}\r\n\r\n" "refreshedOn" = "\r\n📋🔄 Atualizado em: {{ .Time }}\r\n\r\n"
"yes" = "✅ Sim" "yes" = "✅ Sim"
"no" = "❌ Não" "no" = "❌ Não"
"received_id" = "🔑📥 ID atualizado." "received_id" = "🔑📥 ID atualizado."
"received_password" = "🔑📥 Senha atualizada." "received_password" = "🔑📥 Senha atualizada."
"received_email" = "📧📥 E-mail atualizado." "received_email" = "📧📥 E-mail atualizado."
@@ -666,7 +686,6 @@
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ❌ Falhou \n\n🛠 Erro: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ❌ Falhou \n\n🛠 Erro: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Processo de redefinição de tráfego concluído para todos os clientes." "FinishProcess" = "🔚 Processo de redefinição de tráfego concluído para todos os clientes."
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ Fechar teclado" "closeKeyboard" = "❌ Fechar teclado"
"cancel" = "❌ Cancelar" "cancel" = "❌ Cancelar"
@@ -700,7 +719,6 @@
"limitTraffic" = "🚧 Limite de tráfego" "limitTraffic" = "🚧 Limite de tráfego"
"getBanLogs" = "Obter logs de banimento" "getBanLogs" = "Obter logs de banimento"
"allClients" = "Todos os clientes" "allClients" = "Todos os clientes"
"addClient" = "Adicionar Cliente" "addClient" = "Adicionar Cliente"
"submitDisable" = "Enviar como Desativado ☑️" "submitDisable" = "Enviar como Desativado ☑️"
"submitEnable" = "Enviar como Ativado ✅" "submitEnable" = "Enviar como Ativado ✅"
@@ -712,7 +730,6 @@
"ResetAllTraffics" = "Redefinir Todo o Tráfego" "ResetAllTraffics" = "Redefinir Todo o Tráfego"
"SortedTrafficUsageReport" = "Relatório de Uso de Tráfego Ordenado" "SortedTrafficUsageReport" = "Relatório de Uso de Tráfego Ordenado"
[tgbot.answers] [tgbot.answers]
"successfulOperation" = "✅ Operação bem-sucedida!" "successfulOperation" = "✅ Operação bem-sucedida!"
"errorOperation" = "❗ Erro na operação." "errorOperation" = "❗ Erro na operação."

View File

@@ -50,6 +50,7 @@
"fail" = "Ошибка" "fail" = "Ошибка"
"comment" = "Комментарий" "comment" = "Комментарий"
"success" = "Успешно" "success" = "Успешно"
"lastOnline" = "Был(а) в сети"
"getVersion" = "Узнать версию" "getVersion" = "Узнать версию"
"install" = "Установка" "install" = "Установка"
"clients" = "Клиенты" "clients" = "Клиенты"
@@ -71,6 +72,20 @@
"emptyReverseDesc" = "Нет добавленных реверс-прокси." "emptyReverseDesc" = "Нет добавленных реверс-прокси."
"somethingWentWrong" = "Что-то пошло не так" "somethingWentWrong" = "Что-то пошло не так"
[subscription]
"title" = "Информация о подписке"
"subId" = "ID подписки"
"status" = "Статус"
"downloaded" = "Загружено"
"uploaded" = "Отправлено"
"expiry" = "Срок действия"
"totalQuota" = "Общий лимит"
"individualLinks" = "Индивидуальные ссылки"
"active" = "Активна"
"inactive" = "Неактивна"
"unlimited" = "Безлимит"
"noExpiry" = "Без срока"
[menu] [menu]
"theme" = "Тема" "theme" = "Тема"
"dark" = "Темная" "dark" = "Темная"
@@ -84,7 +99,7 @@
[pages.login] [pages.login]
"hello" = "Привет!" "hello" = "Привет!"
"title" = "Добро пожаловать!" "title" = "Приветствие!"
"loginAgain" = "Сессия истекла. Войдите в систему снова" "loginAgain" = "Сессия истекла. Войдите в систему снова"
[pages.login.toasts] [pages.login.toasts]
@@ -117,8 +132,6 @@
"operationHours" = "Время работы системы" "operationHours" = "Время работы системы"
"systemLoad" = "Нагрузка на систему" "systemLoad" = "Нагрузка на систему"
"systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут" "systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут"
"connectionTcpCountDesc" = "Общее количество подключений TCP по всем сетевым картам."
"connectionUdpCountDesc" = "Общее количество подключений UDP по всем сетевым картам."
"connectionCount" = "Количество соединений" "connectionCount" = "Количество соединений"
"ipAddresses" = "IP-адреса сервера" "ipAddresses" = "IP-адреса сервера"
"toggleIpVisibility" = "Переключить видимость IP-адресов сервера" "toggleIpVisibility" = "Переключить видимость IP-адресов сервера"
@@ -134,6 +147,8 @@
"xraySwitchVersionPopover" = "Xray успешно обновлён" "xraySwitchVersionPopover" = "Xray успешно обновлён"
"geofileUpdateDialog" = "Вы действительно хотите обновить геофайл?" "geofileUpdateDialog" = "Вы действительно хотите обновить геофайл?"
"geofileUpdateDialogDesc" = "Это обновит файл #filename#." "geofileUpdateDialogDesc" = "Это обновит файл #filename#."
"geofilesUpdateDialogDesc" = "Это обновит все геофайлы."
"geofilesUpdateAll" = "Обновить все"
"geofileUpdatePopover" = "Геофайл успешно обновлён" "geofileUpdatePopover" = "Геофайл успешно обновлён"
"dontRefresh" = "Установка в процессе. Не обновляйте страницу" "dontRefresh" = "Установка в процессе. Не обновляйте страницу"
"logs" = "Журнал" "logs" = "Журнал"
@@ -151,6 +166,8 @@
"getConfigError" = "Произошла ошибка при получении конфигурационного файла" "getConfigError" = "Произошла ошибка при получении конфигурационного файла"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Общий трафик"
"allTimeTrafficUsage" = "Общее использование за все время"
"title" = "Инбаунды" "title" = "Инбаунды"
"totalDownUp" = "Объем отправленного/полученного трафика" "totalDownUp" = "Объем отправленного/полученного трафика"
"totalUsage" = "Всего трафика" "totalUsage" = "Всего трафика"
@@ -160,10 +177,13 @@
"remark" = "Примечание" "remark" = "Примечание"
"protocol" = "Протокол" "protocol" = "Протокол"
"port" = "Порт" "port" = "Порт"
"portMap" = "Порт-маппинг"
"traffic" = "Трафик" "traffic" = "Трафик"
"details" = "Подробнее" "details" = "Подробнее"
"transportConfig" = "Транспорт" "transportConfig" = "Транспорт"
"expireDate" = "Дата окончания" "expireDate" = "Дата окончания"
"createdAt" = "Создано"
"updatedAt" = "Обновлено"
"resetTraffic" = "Сброс трафика" "resetTraffic" = "Сброс трафика"
"addInbound" = "Создать инбаунд" "addInbound" = "Создать инбаунд"
"generalActions" = "Общие действия" "generalActions" = "Общие действия"
@@ -175,8 +195,6 @@
"deleteClient" = "Удалить клиента" "deleteClient" = "Удалить клиента"
"deleteClientContent" = "Вы уверены, что хотите удалить клиента?" "deleteClientContent" = "Вы уверены, что хотите удалить клиента?"
"resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?" "resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?"
"inboundUpdateSuccess" = "Инбаунд успешно обновлен."
"inboundCreateSuccess" = "Инбаунд успешно создан."
"copyLink" = "Копировать ссылку" "copyLink" = "Копировать ссылку"
"address" = "Адрес" "address" = "Адрес"
"network" = "Сеть" "network" = "Сеть"
@@ -262,6 +280,8 @@
"resetInboundClientTrafficSuccess" = "Трафик сброшен" "resetInboundClientTrafficSuccess" = "Трафик сброшен"
"trafficGetError" = "Ошибка получения данных о трафике" "trafficGetError" = "Ошибка получения данных о трафике"
"getNewX25519CertError" = "Ошибка при получении сертификата X25519." "getNewX25519CertError" = "Ошибка при получении сертификата X25519."
"getNewmldsa65Error" = "Ошибка при получении сертификата mldsa65."
"getNewVlessEncError" = "Ошибка при получении сертификата VlessEnc."
[pages.inbounds.stream.general] [pages.inbounds.stream.general]
"request" = "Запрос" "request" = "Запрос"
@@ -574,6 +594,7 @@
"day" = "День" "day" = "День"
"days" = "Дней" "days" = "Дней"
"hours" = "Часов" "hours" = "Часов"
"minutes" = "Минуты"
"unknown" = "Неизвестно" "unknown" = "Неизвестно"
"inbounds" = "Инбаунды" "inbounds" = "Инбаунды"
"clients" = "Клиенты" "clients" = "Клиенты"
@@ -646,7 +667,6 @@
"refreshedOn" = "\r\n📋🔄 Обновлено: {{ .Time }}\r\n\r\n" "refreshedOn" = "\r\n📋🔄 Обновлено: {{ .Time }}\r\n\r\n"
"yes" = "✅ Да" "yes" = "✅ Да"
"no" = "❌ Нет" "no" = "❌ Нет"
"received_id" = "🔑📥 ID обновлён." "received_id" = "🔑📥 ID обновлён."
"received_password" = "🔑📥 Пароль обновлён." "received_password" = "🔑📥 Пароль обновлён."
"received_email" = "📧📥 Email обновлен." "received_email" = "📧📥 Email обновлен."
@@ -666,7 +686,6 @@
"FailedResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ❌ Неудача \n\n🛠 Ошибка: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ❌ Неудача \n\n🛠 Ошибка: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Сброс трафика завершён для всех клиентов." "FinishProcess" = "🔚 Сброс трафика завершён для всех клиентов."
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ Закрыть клавиатуру" "closeKeyboard" = "❌ Закрыть клавиатуру"
"cancel" = "❌ Отмена" "cancel" = "❌ Отмена"
@@ -700,7 +719,6 @@
"limitTraffic" = "🚧 Лимит трафика" "limitTraffic" = "🚧 Лимит трафика"
"getBanLogs" = "📄 Лог банов" "getBanLogs" = "📄 Лог банов"
"allClients" = "👥 Все клиенты" "allClients" = "👥 Все клиенты"
"addClient" = " Новый клиент" "addClient" = " Новый клиент"
"submitDisable" = "Добавить отключенным ☑️" "submitDisable" = "Добавить отключенным ☑️"
"submitEnable" = "Добавить включенныи ✅" "submitEnable" = "Добавить включенныи ✅"
@@ -712,7 +730,6 @@
"ResetAllTraffics" = "Сбросить весь трафик" "ResetAllTraffics" = "Сбросить весь трафик"
"SortedTrafficUsageReport" = "Отсортированный отчет об использовании трафика" "SortedTrafficUsageReport" = "Отсортированный отчет об использовании трафика"
[tgbot.answers] [tgbot.answers]
"successfulOperation" = "✅ Успешно!" "successfulOperation" = "✅ Успешно!"
"errorOperation" = "❗ Ошибка в операции." "errorOperation" = "❗ Ошибка в операции."

View File

@@ -50,6 +50,7 @@
"fail" = "Başarısız" "fail" = "Başarısız"
"comment" = "Yorum" "comment" = "Yorum"
"success" = "Başarılı" "success" = "Başarılı"
"lastOnline" = "Son çevrimiçi"
"getVersion" = "Sürümü Al" "getVersion" = "Sürümü Al"
"install" = "Yükle" "install" = "Yükle"
"clients" = "Müşteriler" "clients" = "Müşteriler"
@@ -71,6 +72,20 @@
"emptyReverseDesc" = "Eklenmiş ters proxy yok." "emptyReverseDesc" = "Eklenmiş ters proxy yok."
"somethingWentWrong" = "Bir şeyler yanlış gitti" "somethingWentWrong" = "Bir şeyler yanlış gitti"
[subscription]
"title" = "Abonelik Bilgisi"
"subId" = "Abonelik Kimliği"
"status" = "Durum"
"downloaded" = "İndirilen"
"uploaded" = "Yüklenen"
"expiry" = "Son Kullanma"
"totalQuota" = "Toplam Kota"
"individualLinks" = "Bireysel Bağlantılar"
"active" = "Aktif"
"inactive" = "Pasif"
"unlimited" = "Sınırsız"
"noExpiry" = "Süresiz"
[menu] [menu]
"theme" = "Tema" "theme" = "Tema"
"dark" = "Koyu" "dark" = "Koyu"
@@ -117,8 +132,6 @@
"operationHours" = "Çalışma Süresi" "operationHours" = "Çalışma Süresi"
"systemLoad" = "Sistem Yükü" "systemLoad" = "Sistem Yükü"
"systemLoadDesc" = "Geçmiş 1, 5 ve 15 dakika için sistem yük ortalaması" "systemLoadDesc" = "Geçmiş 1, 5 ve 15 dakika için sistem yük ortalaması"
"connectionTcpCountDesc" = "Sistem genelinde toplam TCP bağlantıları"
"connectionUdpCountDesc" = "Sistem genelinde toplam UDP bağlantıları"
"connectionCount" = "Bağlantı İstatistikleri" "connectionCount" = "Bağlantı İstatistikleri"
"ipAddresses" = "IP adresleri" "ipAddresses" = "IP adresleri"
"toggleIpVisibility" = "IP görünürlüğünü değiştir" "toggleIpVisibility" = "IP görünürlüğünü değiştir"
@@ -134,6 +147,8 @@
"xraySwitchVersionPopover" = "Xray başarıyla güncellendi" "xraySwitchVersionPopover" = "Xray başarıyla güncellendi"
"geofileUpdateDialog" = "Geofile'ı gerçekten güncellemek istiyor musunuz?" "geofileUpdateDialog" = "Geofile'ı gerçekten güncellemek istiyor musunuz?"
"geofileUpdateDialogDesc" = "Bu işlem #filename# dosyasını güncelleyecektir." "geofileUpdateDialogDesc" = "Bu işlem #filename# dosyasını güncelleyecektir."
"geofilesUpdateDialogDesc" = "Bu, tüm dosyaları güncelleyecektir."
"geofilesUpdateAll" = "Tümünü güncelle"
"geofileUpdatePopover" = "Geofile başarıyla güncellendi" "geofileUpdatePopover" = "Geofile başarıyla güncellendi"
"dontRefresh" = "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin" "dontRefresh" = "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin"
"logs" = "Günlükler" "logs" = "Günlükler"
@@ -151,6 +166,8 @@
"getConfigError" = "Yapılandırma dosyası alınırken bir hata oluştu" "getConfigError" = "Yapılandırma dosyası alınırken bir hata oluştu"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Toplam Trafik"
"allTimeTrafficUsage" = "Tüm Zamanların Toplam Kullanımı"
"title" = "Gelenler" "title" = "Gelenler"
"totalDownUp" = "Toplam Gönderilen/Alınan" "totalDownUp" = "Toplam Gönderilen/Alınan"
"totalUsage" = "Toplam Kullanım" "totalUsage" = "Toplam Kullanım"
@@ -160,10 +177,13 @@
"remark" = "Açıklama" "remark" = "Açıklama"
"protocol" = "Protokol" "protocol" = "Protokol"
"port" = "Port" "port" = "Port"
"portMap" = "Port Atama"
"traffic" = "Trafik" "traffic" = "Trafik"
"details" = "Detaylar" "details" = "Detaylar"
"transportConfig" = "Taşıma" "transportConfig" = "Taşıma"
"expireDate" = "Süre" "expireDate" = "Süre"
"createdAt" = "Oluşturuldu"
"updatedAt" = "Güncellendi"
"resetTraffic" = "Trafiği Sıfırla" "resetTraffic" = "Trafiği Sıfırla"
"addInbound" = "Gelen Ekle" "addInbound" = "Gelen Ekle"
"generalActions" = "Genel Eylemler" "generalActions" = "Genel Eylemler"
@@ -175,8 +195,6 @@
"deleteClient" = "Müşteriyi Sil" "deleteClient" = "Müşteriyi Sil"
"deleteClientContent" = "Müşteriyi silmek istediğinizden emin misiniz?" "deleteClientContent" = "Müşteriyi silmek istediğinizden emin misiniz?"
"resetTrafficContent" = "Trafiği sıfırlamak istediğinizden emin misiniz?" "resetTrafficContent" = "Trafiği sıfırlamak istediğinizden emin misiniz?"
"inboundUpdateSuccess" = "Gelen bağlantı başarıyla güncellendi."
"inboundCreateSuccess" = "Gelen bağlantı başarıyla oluşturuldu."
"copyLink" = "URL'yi Kopyala" "copyLink" = "URL'yi Kopyala"
"address" = "Adres" "address" = "Adres"
"network" = "Ağ" "network" = "Ağ"
@@ -262,6 +280,8 @@
"resetInboundClientTrafficSuccess" = "Trafik sıfırlandı" "resetInboundClientTrafficSuccess" = "Trafik sıfırlandı"
"trafficGetError" = "Trafik bilgisi alınırken hata oluştu" "trafficGetError" = "Trafik bilgisi alınırken hata oluştu"
"getNewX25519CertError" = "X25519 sertifikası alınırken hata oluştu." "getNewX25519CertError" = "X25519 sertifikası alınırken hata oluştu."
"getNewmldsa65Error" = "mldsa65 sertifikası alınırken hata oluştu."
"getNewVlessEncError" = "VlessEnc sertifikası alınırken hata oluştu."
[pages.inbounds.stream.general] [pages.inbounds.stream.general]
"request" = "İstek" "request" = "İstek"
@@ -561,22 +581,23 @@
"resetOutboundTrafficError" = "Giden trafik sıfırlanırken hata" "resetOutboundTrafficError" = "Giden trafik sıfırlanırken hata"
[tgbot] [tgbot]
"keyboardClosed" = "❌ Özel klavye kapalı!" "keyboardClosed" = "❌ Klavye kapatıldı!"
"noResult" = "❗ Sonuç yok!" "noResult" = "❗ Sonuç yok!"
"noQuery" = "❌ Sorgu bulunamadı! Lütfen komutu tekrar kullanın!" "noQuery" = "❌ Sorgu bulunamadı! Lütfen komutu tekrar kullanın!"
"wentWrong" = "❌ Bir şeyler yanlış gitti!" "wentWrong" = "❌ Bir şeyler yanlış gitti!"
"noIpRecord" = "❗ IP Kaydı yok!" "noIpRecord" = "❗ IP Kaydı Yok!"
"noInbounds" = "❗ Gelen bulunamadı!" "noInbounds" = "❗ Gelen bağlantı bulunamadı!"
"unlimited" = "♾ Sınırsız(Sıfırla)" "unlimited" = "♾ Sınırsız (Sıfırla)"
"add" = "Ekle" "add" = "Ekle"
"month" = "Ay" "month" = "Ay"
"months" = "Aylar" "months" = "Aylar"
"day" = "Gün" "day" = "Gün"
"days" = "Günler" "days" = "Günler"
"hours" = "Saatler" "hours" = "Saatler"
"unknown" = "Bilinmiyor" "minutes" = "Dakika"
"unknown" = "Bilinmeyen"
"inbounds" = "Gelenler" "inbounds" = "Gelenler"
"clients" = "Müşteriler" "clients" = "İstemciler"
"offline" = "🔴 Çevrimdışı" "offline" = "🔴 Çevrimdışı"
"online" = "🟢 Çevrimiçi" "online" = "🟢 Çevrimiçi"
@@ -646,7 +667,6 @@
"refreshedOn" = "\r\n📋🔄 Yenilendi: {{ .Time }}\r\n\r\n" "refreshedOn" = "\r\n📋🔄 Yenilendi: {{ .Time }}\r\n\r\n"
"yes" = "✅ Evet" "yes" = "✅ Evet"
"no" = "❌ Hayır" "no" = "❌ Hayır"
"received_id" = "🔑📥 Kimlik güncellendi." "received_id" = "🔑📥 Kimlik güncellendi."
"received_password" = "🔑📥 Şifre güncellendi." "received_password" = "🔑📥 Şifre güncellendi."
"received_email" = "📧📥 E-posta güncellendi." "received_email" = "📧📥 E-posta güncellendi."
@@ -666,7 +686,6 @@
"FailedResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ❌ Başarısız \n\n🛠 Hata: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ❌ Başarısız \n\n🛠 Hata: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Tüm müşteriler için trafik sıfırlama işlemi tamamlandı." "FinishProcess" = "🔚 Tüm müşteriler için trafik sıfırlama işlemi tamamlandı."
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ Klavyeyi Kapat" "closeKeyboard" = "❌ Klavyeyi Kapat"
"cancel" = "❌ İptal" "cancel" = "❌ İptal"
@@ -700,7 +719,6 @@
"limitTraffic" = "🚧 Trafik Sınırı" "limitTraffic" = "🚧 Trafik Sınırı"
"getBanLogs" = "Yasak Günlüklerini Al" "getBanLogs" = "Yasak Günlüklerini Al"
"allClients" = "Tüm Müşteriler" "allClients" = "Tüm Müşteriler"
"addClient" = "Müşteri Ekle" "addClient" = "Müşteri Ekle"
"submitDisable" = "Devre Dışı Olarak Gönder ☑️" "submitDisable" = "Devre Dışı Olarak Gönder ☑️"
"submitEnable" = "Etkin Olarak Gönder ✅" "submitEnable" = "Etkin Olarak Gönder ✅"
@@ -712,7 +730,6 @@
"ResetAllTraffics" = "Tüm Trafikleri Sıfırla" "ResetAllTraffics" = "Tüm Trafikleri Sıfırla"
"SortedTrafficUsageReport" = "Sıralı Trafik Kullanım Raporu" "SortedTrafficUsageReport" = "Sıralı Trafik Kullanım Raporu"
[tgbot.answers] [tgbot.answers]
"successfulOperation" = "✅ İşlem başarılı!" "successfulOperation" = "✅ İşlem başarılı!"
"errorOperation" = "❗ İşlemde hata." "errorOperation" = "❗ İşlemde hata."

View File

@@ -50,6 +50,7 @@
"fail" = "Помилка" "fail" = "Помилка"
"comment" = "Коментар" "comment" = "Коментар"
"success" = "Успішно" "success" = "Успішно"
"lastOnline" = "Був(ла) онлайн"
"getVersion" = "Отримати версію" "getVersion" = "Отримати версію"
"install" = "Встановити" "install" = "Встановити"
"clients" = "Клієнти" "clients" = "Клієнти"
@@ -71,6 +72,20 @@
"emptyReverseDesc" = "Немає доданих зворотних проксі." "emptyReverseDesc" = "Немає доданих зворотних проксі."
"somethingWentWrong" = "Щось пішло не так" "somethingWentWrong" = "Щось пішло не так"
[subscription]
"title" = "Інформація про підписку"
"subId" = "ID підписки"
"status" = "Статус"
"downloaded" = "Завантажено"
"uploaded" = "Відвантажено"
"expiry" = "Термін дії"
"totalQuota" = "Загальна квота"
"individualLinks" = "Окремі посилання"
"active" = "Активна"
"inactive" = "Неактивна"
"unlimited" = "Безліміт"
"noExpiry" = "Без строку"
[menu] [menu]
"theme" = "Тема" "theme" = "Тема"
"dark" = "Темна" "dark" = "Темна"
@@ -84,7 +99,7 @@
[pages.login] [pages.login]
"hello" = "Привіт" "hello" = "Привіт"
"title" = "Ласкаво просимо" "title" = "Привітання!"
"loginAgain" = "Ваш сеанс закінчився, увійдіть знову" "loginAgain" = "Ваш сеанс закінчився, увійдіть знову"
[pages.login.toasts] [pages.login.toasts]
@@ -117,8 +132,6 @@
"operationHours" = "Час роботи" "operationHours" = "Час роботи"
"systemLoad" = "Завантаження системи" "systemLoad" = "Завантаження системи"
"systemLoadDesc" = "Середнє завантаження системи за останні 1, 5 і 15 хвилин" "systemLoadDesc" = "Середнє завантаження системи за останні 1, 5 і 15 хвилин"
"connectionTcpCountDesc" = "Загальна кількість TCP-з'єднань у системі"
"connectionUdpCountDesc" = "Загальна кількість UDP-з'єднань у системі"
"connectionCount" = "Статистика з'єднання" "connectionCount" = "Статистика з'єднання"
"ipAddresses" = "IP-адреси" "ipAddresses" = "IP-адреси"
"toggleIpVisibility" = "Перемкнути видимість IP" "toggleIpVisibility" = "Перемкнути видимість IP"
@@ -134,6 +147,8 @@
"xraySwitchVersionPopover" = "Xray успішно оновлено" "xraySwitchVersionPopover" = "Xray успішно оновлено"
"geofileUpdateDialog" = "Ви дійсно хочете оновити геофайл?" "geofileUpdateDialog" = "Ви дійсно хочете оновити геофайл?"
"geofileUpdateDialogDesc" = "Це оновить файл #filename#." "geofileUpdateDialogDesc" = "Це оновить файл #filename#."
"geofilesUpdateDialogDesc" = "Це оновить усі геофайли."
"geofilesUpdateAll" = "Оновити все"
"geofileUpdatePopover" = "Геофайл успішно оновлено" "geofileUpdatePopover" = "Геофайл успішно оновлено"
"dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку" "dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку"
"logs" = "Журнали" "logs" = "Журнали"
@@ -151,6 +166,8 @@
"getConfigError" = "Виникла помилка під час отримання файлу конфігурації" "getConfigError" = "Виникла помилка під час отримання файлу конфігурації"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Загальний трафік"
"allTimeTrafficUsage" = "Загальне використання за весь час"
"title" = "Вхідні" "title" = "Вхідні"
"totalDownUp" = "Всього надісланих/отриманих" "totalDownUp" = "Всього надісланих/отриманих"
"totalUsage" = "Всього використанно" "totalUsage" = "Всього використанно"
@@ -160,10 +177,13 @@
"remark" = "Примітка" "remark" = "Примітка"
"protocol" = "Протокол" "protocol" = "Протокол"
"port" = "Порт" "port" = "Порт"
"portMap" = "Порт-перехід"
"traffic" = "Трафік" "traffic" = "Трафік"
"details" = "Деталі" "details" = "Деталі"
"transportConfig" = "Транспорт" "transportConfig" = "Транспорт"
"expireDate" = "Тривалість" "expireDate" = "Тривалість"
"createdAt" = "Створено"
"updatedAt" = "Оновлено"
"resetTraffic" = "Скинути трафік" "resetTraffic" = "Скинути трафік"
"addInbound" = "Додати вхідний" "addInbound" = "Додати вхідний"
"generalActions" = "Загальні дії" "generalActions" = "Загальні дії"
@@ -175,8 +195,6 @@
"deleteClient" = "Видалити клієнта" "deleteClient" = "Видалити клієнта"
"deleteClientContent" = "Ви впевнені, що хочете видалити клієнт?" "deleteClientContent" = "Ви впевнені, що хочете видалити клієнт?"
"resetTrafficContent" = "Ви впевнені, що хочете скинути трафік?" "resetTrafficContent" = "Ви впевнені, що хочете скинути трафік?"
"inboundUpdateSuccess" = "Вхідне підключення успішно оновлено."
"inboundCreateSuccess" = "Вхідне підключення успішно створено."
"copyLink" = "Копіювати URL" "copyLink" = "Копіювати URL"
"address" = "Адреса" "address" = "Адреса"
"network" = "Мережа" "network" = "Мережа"
@@ -262,6 +280,8 @@
"resetInboundClientTrafficSuccess" = "Трафік скинуто" "resetInboundClientTrafficSuccess" = "Трафік скинуто"
"trafficGetError" = "Помилка отримання даних про трафік" "trafficGetError" = "Помилка отримання даних про трафік"
"getNewX25519CertError" = "Помилка при отриманні сертифіката X25519." "getNewX25519CertError" = "Помилка при отриманні сертифіката X25519."
"getNewmldsa65Error" = "Помилка при отриманні сертифіката mldsa65."
"getNewVlessEncError" = "Помилка при отриманні сертифіката VlessEnc."
[pages.inbounds.stream.general] [pages.inbounds.stream.general]
"request" = "Запит" "request" = "Запит"
@@ -561,19 +581,20 @@
"resetOutboundTrafficError" = "Помилка скидання вихідного трафіку" "resetOutboundTrafficError" = "Помилка скидання вихідного трафіку"
[tgbot] [tgbot]
"keyboardClosed" = "❌ Спеціальна клавіатура закрита!" "keyboardClosed" = "❌ Клавіатуру закрито!"
"noResult" = "❗ Немає результату!" "noResult" = "❗ Немає результату!"
"noQuery" = "❌ Запит не знайдено! Скористайтеся командою ще раз!" "noQuery" = "❌ Запит не знайдено! Будь ласка, використовуйте команду ще раз!"
"wentWrong" = "❌ Щось пішло не так!" "wentWrong" = "❌ Щось пішло не так!"
"noIpRecord" = "❗ Немає IP-запису!" "noIpRecord" = "❗ Немає запису IP!"
"noInbounds" = "❗ Вхідних не знайдено!" "noInbounds" = "❗ Вхідні не знайдені!"
"unlimited" = "♾ Необмежений (скинути)" "unlimited" = "♾ Необмежено (Скинути)"
"add" = "Додати" "add" = "Додати"
"month" = "Місяць" "month" = "Місяць"
"months" = "Місяці" "months" = "Місяці"
"day" = "День" "day" = "День"
"days" = "Дні" "days" = "Дні"
"hours" = "Годинник" "hours" = "Години"
"minutes" = "Хвилини"
"unknown" = "Невідомо" "unknown" = "Невідомо"
"inbounds" = "Вхідні" "inbounds" = "Вхідні"
"clients" = "Клієнти" "clients" = "Клієнти"
@@ -646,7 +667,6 @@
"refreshedOn" = "\r\n📋🔄 Оновлено: {{ .Time }}\r\n\r\n" "refreshedOn" = "\r\n📋🔄 Оновлено: {{ .Time }}\r\n\r\n"
"yes" = "✅ Так" "yes" = "✅ Так"
"no" = "❌ Ні" "no" = "❌ Ні"
"received_id" = "🔑📥 ID оновлено." "received_id" = "🔑📥 ID оновлено."
"received_password" = "🔑📥 Пароль оновлено." "received_password" = "🔑📥 Пароль оновлено."
"received_email" = "📧📥 Електронна пошта оновлена." "received_email" = "📧📥 Електронна пошта оновлена."
@@ -666,7 +686,6 @@
"FailedResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ❌ Невдача \n\n🛠 Помилка: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ❌ Невдача \n\n🛠 Помилка: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Процес скидання трафіку завершено для всіх клієнтів." "FinishProcess" = "🔚 Процес скидання трафіку завершено для всіх клієнтів."
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ Закрити клавіатуру" "closeKeyboard" = "❌ Закрити клавіатуру"
"cancel" = "❌ Скасувати" "cancel" = "❌ Скасувати"
@@ -700,7 +719,6 @@
"limitTraffic" = "🚧 Ліміт трафіку" "limitTraffic" = "🚧 Ліміт трафіку"
"getBanLogs" = "Отримати журнали заборон" "getBanLogs" = "Отримати журнали заборон"
"allClients" = "Всі Клієнти" "allClients" = "Всі Клієнти"
"addClient" = "Додати клієнта" "addClient" = "Додати клієнта"
"submitDisable" = "Надіслати як вимкнено ☑️" "submitDisable" = "Надіслати як вимкнено ☑️"
"submitEnable" = "Надіслати як увімкнено ✅" "submitEnable" = "Надіслати як увімкнено ✅"
@@ -712,7 +730,6 @@
"ResetAllTraffics" = "Скинути весь трафік" "ResetAllTraffics" = "Скинути весь трафік"
"SortedTrafficUsageReport" = "Відсортований звіт про використання трафіку" "SortedTrafficUsageReport" = "Відсортований звіт про використання трафіку"
[tgbot.answers] [tgbot.answers]
"successfulOperation" = "✅ Операція успішна!" "successfulOperation" = "✅ Операція успішна!"
"errorOperation" = "❗ Помилка в роботі." "errorOperation" = "❗ Помилка в роботі."

View File

@@ -50,6 +50,7 @@
"fail" = "Thất bại" "fail" = "Thất bại"
"comment" = "Bình luận" "comment" = "Bình luận"
"success" = "Thành công" "success" = "Thành công"
"lastOnline" = "Lần online gần nhất"
"getVersion" = "Lấy phiên bản" "getVersion" = "Lấy phiên bản"
"install" = "Cài đặt" "install" = "Cài đặt"
"clients" = "Các khách hàng" "clients" = "Các khách hàng"
@@ -71,6 +72,20 @@
"emptyReverseDesc" = "Không có proxy ngược nào được thêm." "emptyReverseDesc" = "Không có proxy ngược nào được thêm."
"somethingWentWrong" = "Đã xảy ra lỗi" "somethingWentWrong" = "Đã xảy ra lỗi"
[subscription]
"title" = "Thông tin đăng ký"
"subId" = "ID đăng ký"
"status" = "Trạng thái"
"downloaded" = "Đã tải xuống"
"uploaded" = "Đã tải lên"
"expiry" = "Hết hạn"
"totalQuota" = "Tổng hạn mức"
"individualLinks" = "Liên kết riêng lẻ"
"active" = "Hoạt động"
"inactive" = "Không hoạt động"
"unlimited" = "Không giới hạn"
"noExpiry" = "Không hết hạn"
[menu] [menu]
"theme" = "Chủ đề" "theme" = "Chủ đề"
"dark" = "Tối" "dark" = "Tối"
@@ -91,7 +106,7 @@
"invalidFormData" = "Dạng dữ liệu nhập không hợp lệ." "invalidFormData" = "Dạng dữ liệu nhập không hợp lệ."
"emptyUsername" = "Vui lòng nhập tên người dùng." "emptyUsername" = "Vui lòng nhập tên người dùng."
"emptyPassword" = "Vui lòng nhập mật khẩu." "emptyPassword" = "Vui lòng nhập mật khẩu."
"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ." "wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ."
"successLogin" = "Bạn đã đăng nhập vào tài khoản thành công." "successLogin" = "Bạn đã đăng nhập vào tài khoản thành công."
[pages.index] [pages.index]
@@ -117,8 +132,6 @@
"operationHours" = "Thời gian hoạt động" "operationHours" = "Thời gian hoạt động"
"systemLoad" = "Tải hệ thống" "systemLoad" = "Tải hệ thống"
"systemLoadDesc" = "trung bình tải hệ thống trong 1, 5 và 15 phút qua" "systemLoadDesc" = "trung bình tải hệ thống trong 1, 5 và 15 phút qua"
"connectionTcpCountDesc" = "Tổng số kết nối TCP trên tất cả các 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" "connectionCount" = "Số lượng kết nối"
"ipAddresses" = "Địa chỉ IP" "ipAddresses" = "Địa chỉ IP"
"toggleIpVisibility" = "Chuyển đổi hiển thị IP" "toggleIpVisibility" = "Chuyển đổi hiển thị IP"
@@ -134,6 +147,8 @@
"xraySwitchVersionPopover" = "Xray đã được cập nhật thành công" "xraySwitchVersionPopover" = "Xray đã được cập nhật thành công"
"geofileUpdateDialog" = "Bạn có chắc chắn muốn cập nhật geofile không?" "geofileUpdateDialog" = "Bạn có chắc chắn muốn cập nhật geofile không?"
"geofileUpdateDialogDesc" = "Hành động này sẽ cập nhật tệp #filename#." "geofileUpdateDialogDesc" = "Hành động này sẽ cập nhật tệp #filename#."
"geofilesUpdateDialogDesc" = "Thao tác này sẽ cập nhật tất cả các tập tin."
"geofilesUpdateAll" = "Cập nhật tất cả"
"geofileUpdatePopover" = "Geofile đã được cập nhật thành công" "geofileUpdatePopover" = "Geofile đã được cập nhật thành công"
"dontRefresh" = "Đang tiến hành cài đặt, vui lòng không làm mới trang này." "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ý" "logs" = "Nhật ký"
@@ -151,6 +166,8 @@
"getConfigError" = "Lỗi xảy ra khi truy xuất tệp cấu hình" "getConfigError" = "Lỗi xảy ra khi truy xuất tệp cấu hình"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Tổng Lưu Lượng"
"allTimeTrafficUsage" = "Tổng mức sử dụng mọi lúc"
"title" = "Điểm vào (Inbounds)" "title" = "Điểm vào (Inbounds)"
"totalDownUp" = "Tổng tải lên/tải xuống" "totalDownUp" = "Tổng tải lên/tải xuống"
"totalUsage" = "Tổng sử dụng" "totalUsage" = "Tổng sử dụng"
@@ -160,10 +177,13 @@
"remark" = "Chú thích" "remark" = "Chú thích"
"protocol" = "Giao thức" "protocol" = "Giao thức"
"port" = "Cổng" "port" = "Cổng"
"portMap" = "Cổng tạo"
"traffic" = "Lưu lượng" "traffic" = "Lưu lượng"
"details" = "Chi tiết" "details" = "Chi tiết"
"transportConfig" = "Giao vận" "transportConfig" = "Giao vận"
"expireDate" = "Ngày hết hạn" "expireDate" = "Ngày hết hạn"
"createdAt" = "Tạo lúc"
"updatedAt" = "Cập nhật"
"resetTraffic" = "Đặt lại lưu lượng" "resetTraffic" = "Đặt lại lưu lượng"
"addInbound" = "Thêm điểm vào" "addInbound" = "Thêm điểm vào"
"generalActions" = "Hành động chung" "generalActions" = "Hành động chung"
@@ -175,8 +195,6 @@
"deleteClient" = "Xóa người dùng" "deleteClient" = "Xóa người dùng"
"deleteClientContent" = "Bạn có chắc chắn muốn xóa người dùng không?" "deleteClientContent" = "Bạn có chắc chắn muốn xóa người dùng không?"
"resetTrafficContent" = "Xác nhận đặt lại lưu lượng?" "resetTrafficContent" = "Xác nhận đặt lại lưu lượng?"
"inboundUpdateSuccess" = "Đã cập nhật kết nối inbound thành công."
"inboundCreateSuccess" = "Đã tạo kết nối inbound thành công."
"copyLink" = "Sao chép liên kết" "copyLink" = "Sao chép liên kết"
"address" = "Địa chỉ" "address" = "Địa chỉ"
"network" = "Mạng" "network" = "Mạng"
@@ -262,6 +280,8 @@
"resetInboundClientTrafficSuccess" = "Đã đặt lại lưu lượng" "resetInboundClientTrafficSuccess" = "Đã đặt lại lưu lượng"
"trafficGetError" = "Lỗi khi lấy thông tin lưu lượng" "trafficGetError" = "Lỗi khi lấy thông tin lưu lượng"
"getNewX25519CertError" = "Lỗi khi lấy chứng chỉ X25519." "getNewX25519CertError" = "Lỗi khi lấy chứng chỉ X25519."
"getNewmldsa65Error" = "Lỗi khi lấy chứng chỉ mldsa65."
"getNewVlessEncError" = "Lỗi khi lấy chứng chỉ VlessEnc."
[pages.inbounds.stream.general] [pages.inbounds.stream.general]
"request" = "Lời yêu cầu" "request" = "Lời yêu cầu"
@@ -535,9 +555,9 @@
[pages.settings.security] [pages.settings.security]
"admin" = "Thông tin đăng nhập quản trị viên" "admin" = "Thông tin đăng nhập quản trị viên"
"twoFactor" = "Xác thực hai yếu tố" "twoFactor" = "Xác thực hai yếu tố"
"twoFactorEnable" = "Bật 2FA" "twoFactorEnable" = "Bật 2FA"
"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn." "twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn."
"twoFactorModalSetTitle" = "Bật xác thực hai yếu tố" "twoFactorModalSetTitle" = "Bật xác thực hai yếu tố"
"twoFactorModalDeleteTitle" = "Tắt xác thực hai yếu tố" "twoFactorModalDeleteTitle" = "Tắt xác thực hai yếu tố"
"twoFactorModalSteps" = "Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:" "twoFactorModalSteps" = "Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:"
@@ -561,22 +581,23 @@
"resetOutboundTrafficError" = "Lỗi khi đặt lại lưu lượng truy cập đi" "resetOutboundTrafficError" = "Lỗi khi đặt lại lưu lượng truy cập đi"
[tgbot] [tgbot]
"keyboardClosed" = "❌ Bàn phím tùy chỉnh đã đóng!" "keyboardClosed" = "❌ Bàn phím đã đóng!"
"noResult" = "❗ Không có kết quả!" "noResult" = "❗ Không có kết quả!"
"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lệnh lại!" "noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lại lệnh!"
"wentWrong" = "❌ Đã xảy ra lỗi!" "wentWrong" = "❌ Đã xảy ra lỗi!"
"noIpRecord" = "❗ Không có bản ghi IP!" "noIpRecord" = "❗ Không có bản ghi IP!"
"noInbounds" = "❗ Không tìm thấy inbound!" "noInbounds" = "❗ Không tìm thấy inbound!"
"unlimited" = "♾ Không giới hạn" "unlimited" = "♾ Không giới hạn (Đặt lại)"
"add" = "Thêm" "add" = "Thêm"
"month" = "Tháng" "month" = "Tháng"
"months" = "Tháng" "months" = "Tháng"
"day" = "Ngày" "day" = "Ngày"
"days" = "Ngày" "days" = "Ngày"
"hours" = "Giờ" "hours" = "Giờ"
"unknown" = "Không rõ" "minutes" = "Phút"
"inbounds" = "Vào" "unknown" = "Không xác định"
"clients" = "Các người dùng" "inbounds" = "Inbound"
"clients" = "Client"
"offline" = "🔴 Ngoại tuyến" "offline" = "🔴 Ngoại tuyến"
"online" = "🟢 Trực tuyến" "online" = "🟢 Trực tuyến"
@@ -646,7 +667,6 @@
"refreshedOn" = "\r\n📋🔄 Đã cập nhật lần cuối vào: {{ .Time }}\r\n\r\n" "refreshedOn" = "\r\n📋🔄 Đã cập nhật lần cuối vào: {{ .Time }}\r\n\r\n"
"yes" = "✅ Có" "yes" = "✅ Có"
"no" = "❌ Không" "no" = "❌ Không"
"received_id" = "🔑📥 ID đã được cập nhật." "received_id" = "🔑📥 ID đã được cập nhật."
"received_password" = "🔑📥 Mật khẩu đã được cập nhật." "received_password" = "🔑📥 Mật khẩu đã được cập nhật."
"received_email" = "📧📥 Email đã được cập nhật." "received_email" = "📧📥 Email đã được cập nhật."
@@ -666,7 +686,6 @@
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ❌ Thất bại \n\n🛠 Lỗi: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ❌ Thất bại \n\n🛠 Lỗi: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Quá trình đặt lại lưu lượng đã hoàn tất cho tất cả khách hàng." "FinishProcess" = "🔚 Quá trình đặt lại lưu lượng đã hoàn tất cho tất cả khách hàng."
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ Đóng Bàn Phím" "closeKeyboard" = "❌ Đóng Bàn Phím"
"cancel" = "❌ Hủy" "cancel" = "❌ Hủy"
@@ -700,7 +719,6 @@
"limitTraffic" = "🚧 Giới hạn lưu lượng" "limitTraffic" = "🚧 Giới hạn lưu lượng"
"getBanLogs" = "Cấm nhật ký" "getBanLogs" = "Cấm nhật ký"
"allClients" = "Tất cả Khách hàng" "allClients" = "Tất cả Khách hàng"
"addClient" = "Thêm Khách Hàng" "addClient" = "Thêm Khách Hàng"
"submitDisable" = "Gửi Dưới Dạng Vô Hiệu ☑️" "submitDisable" = "Gửi Dưới Dạng Vô Hiệu ☑️"
"submitEnable" = "Gửi Dưới Dạng Kích Hoạt ✅" "submitEnable" = "Gửi Dưới Dạng Kích Hoạt ✅"
@@ -712,7 +730,6 @@
"ResetAllTraffics" = "Đặt lại tất cả lưu lượng" "ResetAllTraffics" = "Đặt lại tất cả lưu lượng"
"SortedTrafficUsageReport" = "Báo cáo sử dụng lưu lượng đã sắp xếp" "SortedTrafficUsageReport" = "Báo cáo sử dụng lưu lượng đã sắp xếp"
[tgbot.answers] [tgbot.answers]
"successfulOperation" = "✅ Thành công!" "successfulOperation" = "✅ Thành công!"
"errorOperation" = "❗ Lỗi Trong Quá Trình Thực Hiện." "errorOperation" = "❗ Lỗi Trong Quá Trình Thực Hiện."

View File

@@ -50,6 +50,7 @@
"fail" = "失败" "fail" = "失败"
"comment" = "评论" "comment" = "评论"
"success" = "成功" "success" = "成功"
"lastOnline" = "上次在线"
"getVersion" = "获取版本" "getVersion" = "获取版本"
"install" = "安装" "install" = "安装"
"clients" = "客户端" "clients" = "客户端"
@@ -71,6 +72,20 @@
"emptyReverseDesc" = "未添加反向代理。" "emptyReverseDesc" = "未添加反向代理。"
"somethingWentWrong" = "出了点问题" "somethingWentWrong" = "出了点问题"
[subscription]
"title" = "订阅信息"
"subId" = "订阅 ID"
"status" = "状态"
"downloaded" = "已下载"
"uploaded" = "已上传"
"expiry" = "到期"
"totalQuota" = "总配额"
"individualLinks" = "单独链接"
"active" = "启用"
"inactive" = "停用"
"unlimited" = "无限制"
"noExpiry" = "无到期"
[menu] [menu]
"theme" = "主题" "theme" = "主题"
"dark" = "暗色" "dark" = "暗色"
@@ -117,8 +132,6 @@
"operationHours" = "系统正常运行时间" "operationHours" = "系统正常运行时间"
"systemLoad" = "系统负载" "systemLoad" = "系统负载"
"systemLoadDesc" = "过去 1、5 和 15 分钟的系统平均负载" "systemLoadDesc" = "过去 1、5 和 15 分钟的系统平均负载"
"connectionTcpCountDesc" = "系统中所有 TCP 连接数"
"connectionUdpCountDesc" = "系统中所有 UDP 连接数"
"connectionCount" = "连接数" "connectionCount" = "连接数"
"ipAddresses" = "IP地址" "ipAddresses" = "IP地址"
"toggleIpVisibility" = "切换IP可见性" "toggleIpVisibility" = "切换IP可见性"
@@ -134,6 +147,8 @@
"xraySwitchVersionPopover" = "Xray 更新成功" "xraySwitchVersionPopover" = "Xray 更新成功"
"geofileUpdateDialog" = "您确定要更新地理文件吗?" "geofileUpdateDialog" = "您确定要更新地理文件吗?"
"geofileUpdateDialogDesc" = "这将更新 #filename# 文件。" "geofileUpdateDialogDesc" = "这将更新 #filename# 文件。"
"geofilesUpdateDialogDesc" = "这将更新所有文件。"
"geofilesUpdateAll" = "全部更新"
"geofileUpdatePopover" = "地理文件更新成功" "geofileUpdatePopover" = "地理文件更新成功"
"dontRefresh" = "安装中,请勿刷新此页面" "dontRefresh" = "安装中,请勿刷新此页面"
"logs" = "日志" "logs" = "日志"
@@ -151,6 +166,8 @@
"getConfigError" = "检索配置文件时出错" "getConfigError" = "检索配置文件时出错"
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "累计总流量"
"allTimeTrafficUsage" = "所有时间总使用量"
"title" = "入站列表" "title" = "入站列表"
"totalDownUp" = "总上传 / 下载" "totalDownUp" = "总上传 / 下载"
"totalUsage" = "总用量" "totalUsage" = "总用量"
@@ -160,10 +177,13 @@
"remark" = "备注" "remark" = "备注"
"protocol" = "协议" "protocol" = "协议"
"port" = "端口" "port" = "端口"
"portMap" = "端口映射"
"traffic" = "流量" "traffic" = "流量"
"details" = "详细信息" "details" = "详细信息"
"transportConfig" = "传输配置" "transportConfig" = "传输配置"
"expireDate" = "到期时间" "expireDate" = "到期时间"
"createdAt" = "创建时间"
"updatedAt" = "更新时间"
"resetTraffic" = "重置流量" "resetTraffic" = "重置流量"
"addInbound" = "添加入站" "addInbound" = "添加入站"
"generalActions" = "通用操作" "generalActions" = "通用操作"
@@ -175,8 +195,6 @@
"deleteClient" = "删除客户端" "deleteClient" = "删除客户端"
"deleteClientContent" = "确定要删除客户端吗?" "deleteClientContent" = "确定要删除客户端吗?"
"resetTrafficContent" = "确定要重置流量吗?" "resetTrafficContent" = "确定要重置流量吗?"
"inboundUpdateSuccess" = "入站连接已成功更新。"
"inboundCreateSuccess" = "入站连接已成功创建。"
"copyLink" = "复制链接" "copyLink" = "复制链接"
"address" = "地址" "address" = "地址"
"network" = "网络" "network" = "网络"
@@ -262,6 +280,8 @@
"resetInboundClientTrafficSuccess" = "流量已重置" "resetInboundClientTrafficSuccess" = "流量已重置"
"trafficGetError" = "获取流量数据时出错" "trafficGetError" = "获取流量数据时出错"
"getNewX25519CertError" = "获取X25519证书时出错。" "getNewX25519CertError" = "获取X25519证书时出错。"
"getNewmldsa65Error" = "获取mldsa65证书时出错。"
"getNewVlessEncError" = "获取VlessEnc证书时出错。"
[pages.inbounds.stream.general] [pages.inbounds.stream.general]
"request" = "请求" "request" = "请求"
@@ -563,19 +583,20 @@
[tgbot] [tgbot]
"keyboardClosed" = "❌ 自定义键盘已关闭!" "keyboardClosed" = "❌ 自定义键盘已关闭!"
"noResult" = "❗ 没有结果!" "noResult" = "❗ 没有结果!"
"noQuery" = "❌ 未找到查询!请重新使用命令!" "noQuery" = "❌ 未找到查询!请再次使用命令!"
"wentWrong" = "❌ 出了点问题!" "wentWrong" = "❌ 出了点问题!"
"noIpRecord" = "❗ 没有 IP 记录!" "noIpRecord" = "❗ 没有IP记录"
"noInbounds" = "❗ 没有找到入站连接" "noInbounds" = "❗ 找到入站!"
"unlimited" = "♾ 无限" "unlimited" = "♾ 无限(重置)"
"add" = "添加" "add" = "添加"
"month" = "月" "month" = "月"
"months" = "月" "months" = "月"
"day" = "天" "day" = "天"
"days" = "天" "days" = "天"
"hours" = "小时" "hours" = "小时"
"minutes" = "分钟"
"unknown" = "未知" "unknown" = "未知"
"inbounds" = "入站连接" "inbounds" = "入站"
"clients" = "客户端" "clients" = "客户端"
"offline" = "🔴 离线" "offline" = "🔴 离线"
"online" = "🟢 在线" "online" = "🟢 在线"
@@ -646,7 +667,6 @@
"refreshedOn" = "\r\n📋🔄 刷新时间:{{ .Time }}\r\n\r\n" "refreshedOn" = "\r\n📋🔄 刷新时间:{{ .Time }}\r\n\r\n"
"yes" = "✅ 是的" "yes" = "✅ 是的"
"no" = "❌ 没有" "no" = "❌ 没有"
"received_id" = "🔑📥 ID 已更新。" "received_id" = "🔑📥 ID 已更新。"
"received_password" = "🔑📥 密码已更新。" "received_password" = "🔑📥 密码已更新。"
"received_email" = "📧📥 邮箱已更新。" "received_email" = "📧📥 邮箱已更新。"
@@ -666,7 +686,6 @@
"FailedResetTraffic" = "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ❌ 失败 \n\n🛠 错误: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ❌ 失败 \n\n🛠 错误: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 所有客户的流量重置已完成。" "FinishProcess" = "🔚 所有客户的流量重置已完成。"
[tgbot.buttons] [tgbot.buttons]
"closeKeyboard" = "❌ 关闭键盘" "closeKeyboard" = "❌ 关闭键盘"
"cancel" = "❌ 取消" "cancel" = "❌ 取消"
@@ -700,7 +719,6 @@
"limitTraffic" = "🚧 流量限制" "limitTraffic" = "🚧 流量限制"
"getBanLogs" = "禁止日志" "getBanLogs" = "禁止日志"
"allClients" = "所有客户" "allClients" = "所有客户"
"addClient" = "添加客户" "addClient" = "添加客户"
"submitDisable" = "提交为禁用 ☑️" "submitDisable" = "提交为禁用 ☑️"
"submitEnable" = "提交为启用 ✅" "submitEnable" = "提交为启用 ✅"
@@ -712,7 +730,6 @@
"ResetAllTraffics" = "重置所有流量" "ResetAllTraffics" = "重置所有流量"
"SortedTrafficUsageReport" = "排序的流量使用报告" "SortedTrafficUsageReport" = "排序的流量使用报告"
[tgbot.answers] [tgbot.answers]
"successfulOperation" = "✅ 成功!" "successfulOperation" = "✅ 成功!"
"errorOperation" = "❗ 操作错误。" "errorOperation" = "❗ 操作错误。"

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