Compare commits
234 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6b3623634 | ||
|
|
947fd4fae1 | ||
|
|
e69a31dd59 | ||
|
|
719ae0e014 | ||
|
|
5bcf6a8aeb | ||
|
|
945fefde12 | ||
|
|
313a2acbf6 | ||
|
|
b747730211 | ||
|
|
692a73788a | ||
|
|
3287fa4d80 | ||
|
|
1393f981bc | ||
|
|
9a2c1c6b43 | ||
|
|
278aa1c85c | ||
|
|
8fe297ef9d | ||
|
|
c881d1015a | ||
|
|
c061337ce7 | ||
|
|
260eedf8c4 | ||
|
|
69ccdba734 | ||
|
|
4c797dc154 | ||
|
|
f000322a06 | ||
|
|
0ea8b5352a | ||
|
|
68240061aa | ||
|
|
0695f677ba | ||
|
|
70f6d6b21a | ||
|
|
e8c509c720 | ||
|
|
83a1c721c7 | ||
|
|
7ccc0877a1 | ||
|
|
ad659e48cf | ||
|
|
784ed39930 | ||
|
|
538f7fd5d7 | ||
|
|
cf38226b5d | ||
|
|
575ee854c8 | ||
|
|
9936af80dd | ||
|
|
4a75bd0a48 | ||
|
|
b0c223c631 | ||
|
|
313b51f96f | ||
|
|
020cd63e22 | ||
|
|
6e46e9b16e | ||
|
|
713a7328f6 | ||
|
|
01d4a7488d | ||
|
|
2b2ed3349a | ||
|
|
d8523bbdac | ||
|
|
8afa39144e | ||
|
|
00baeffe74 | ||
|
|
b578a33518 | ||
|
|
8153e0ac05 | ||
|
|
2eb9d2e2e8 | ||
|
|
a824875c4f | ||
|
|
cafcb250ec | ||
|
|
e7cfee570b | ||
|
|
90c3529301 | ||
|
|
b65ec83c39 | ||
|
|
28a17a80ec | ||
|
|
3056583388 | ||
|
|
172f2ddaa7 | ||
|
|
d69af328dc | ||
|
|
ee0e3093ba | ||
|
|
89def9aee6 | ||
|
|
b2b0024648 | ||
|
|
5822758b7c | ||
|
|
49430b3991 | ||
|
|
104526aab2 | ||
|
|
a0c07241c0 | ||
|
|
adf3242602 | ||
|
|
3f62592e4b | ||
|
|
02bff4db6c | ||
|
|
8ff4e1ff31 | ||
|
|
26c6438ec2 | ||
|
|
b3e96230c4 | ||
|
|
1016f3b4f9 | ||
|
|
020bc9d77c | ||
|
|
5620d739c6 | ||
|
|
d518979e4f | ||
|
|
83f8a03b50 | ||
|
|
b45e63a14a | ||
|
|
3007bcff97 | ||
|
|
55f1d72af5 | ||
|
|
806ecbd7c5 | ||
|
|
ae79b43cdb | ||
|
|
e64e6327ef | ||
|
|
9f024b9e6a | ||
|
|
eacfbc86b5 | ||
|
|
37c17357fc | ||
|
|
b35d339665 | ||
|
|
5e7a3db873 | ||
|
|
6ced549dea | ||
|
|
f60682a6b7 | ||
|
|
50bd7a8040 | ||
|
|
7465768ff7 | ||
|
|
5b00a52c65 | ||
|
|
151f1173a1 | ||
|
|
e262132b9d | ||
|
|
ca0a7aeb5a | ||
|
|
7447cec17e | ||
|
|
0ffd27c0aa | ||
|
|
054cb1dea0 | ||
|
|
3757ae0b11 | ||
|
|
e3883fca87 | ||
|
|
b46a0b404b | ||
|
|
0ce58a095a | ||
|
|
59ea2645db | ||
|
|
8c8d280f14 | ||
|
|
c720008187 | ||
|
|
170d24499e | ||
|
|
99c79d4056 | ||
|
|
fcdeb1fc79 | ||
|
|
0a58b5e745 | ||
|
|
db7e7dcd29 | ||
|
|
01b8a27996 | ||
|
|
3764ece26c | ||
|
|
d7efc2aef9 | ||
|
|
2eb8abf61e | ||
|
|
299572a4c2 | ||
|
|
22afa50901 | ||
|
|
bc274d1e1f | ||
|
|
dc21f41932 | ||
|
|
f137b1af76 | ||
|
|
c4871ef8fe | ||
|
|
ecfffa882a | ||
|
|
3af5026abe | ||
|
|
1de7accd7c | ||
|
|
76afff2a6f | ||
|
|
9623e87511 | ||
|
|
bc0518391e | ||
|
|
5408a2f82c | ||
|
|
c8d71ea748 | ||
|
|
46de886b53 | ||
|
|
6d41320ed7 | ||
|
|
bf9d2e6aeb | ||
|
|
ed96fa090b | ||
|
|
3ac1d7f546 | ||
|
|
10025ffa66 | ||
|
|
5ee62b25ca | ||
|
|
311d11a3c1 | ||
|
|
40b6d7707a | ||
|
|
cbf316db31 | ||
|
|
33a36ada4b | ||
|
|
82ddd10627 | ||
|
|
2401c99817 | ||
|
|
2f36a4047c | ||
|
|
dc3b0d218a | ||
|
|
610d29765a | ||
|
|
b1ea8005e4 | ||
|
|
3f0bfa2472 | ||
|
|
1e2ff650ad | ||
|
|
c2d6dd923f | ||
|
|
723ec25fb2 | ||
|
|
7dc52e9a53 | ||
|
|
fe9f0d1d0e | ||
|
|
18d74d54ca | ||
|
|
c7ba6ae909 | ||
|
|
3edf79e589 | ||
|
|
5420e643cf | ||
|
|
9fcd0387ca | ||
|
|
7b039d219e | ||
|
|
dbec28b915 | ||
|
|
e5126806d7 | ||
|
|
b008ff4ad2 | ||
|
|
da6b89fdcd | ||
|
|
d7882c25d1 | ||
|
|
ed2a0a0bcf | ||
|
|
4a0914cb1e | ||
|
|
664269d513 | ||
|
|
d0796b26c9 | ||
|
|
2750f46c01 | ||
|
|
023eb513e4 | ||
|
|
0c7b59ed47 | ||
|
|
3087c1b123 | ||
|
|
2198397197 | ||
|
|
d10c312e62 | ||
|
|
24a3411465 | ||
|
|
2198e7a28f | ||
|
|
6b23b416a7 | ||
|
|
16f53ce4c2 | ||
|
|
27445b30e9 | ||
|
|
3d0212c21d | ||
|
|
978755960f | ||
|
|
9b51e9a5c5 | ||
|
|
6879a8fbcb | ||
|
|
7258841491 | ||
|
|
23dd80fbb0 | ||
|
|
6556884c7f | ||
|
|
d5c532c64f | ||
|
|
ad5f774a1e | ||
|
|
aa285914fa | ||
|
|
4d02756e1e | ||
|
|
825d93d95f | ||
|
|
5ea6386815 | ||
|
|
d064e85ecd | ||
|
|
9fc03bd10a | ||
|
|
ae08a29cde | ||
|
|
4f25eb230e | ||
|
|
ce72d53d1a | ||
|
|
5e641ff9e8 | ||
|
|
58898e5758 | ||
|
|
569550d5f6 | ||
|
|
419ea63dd0 | ||
|
|
6a17285935 | ||
|
|
4b03e9d919 | ||
|
|
7e9c3bdbaf | ||
|
|
957f3dbb54 | ||
|
|
05e60af283 | ||
|
|
5e40458116 | ||
|
|
baf6fdd29d | ||
|
|
45f78d3521 | ||
|
|
01f984e054 | ||
|
|
e4ba5ba53a | ||
|
|
6ff555c8bb | ||
|
|
3c1634ca7c | ||
|
|
561c4810be | ||
|
|
eb1b96643d | ||
|
|
de5314c01f | ||
|
|
1088d1faf3 | ||
|
|
267024c43f | ||
|
|
0d595f56e4 | ||
|
|
a4c4f9efb3 | ||
|
|
73a5722cca | ||
|
|
30264043f8 | ||
|
|
c6062eb15c | ||
|
|
f1b7944828 | ||
|
|
7a57b31ff3 | ||
|
|
6e1b949081 | ||
|
|
0ad708b1b6 | ||
|
|
71f13ebcbd | ||
|
|
f5f4a530cc | ||
|
|
702f03e4b7 | ||
|
|
487ec74e0b | ||
|
|
b4dae36345 | ||
|
|
761728255c | ||
|
|
b1ab156e42 | ||
|
|
fa45bf87de | ||
|
|
75416eebd7 | ||
|
|
87042d77ba | ||
|
|
011e0f309a |
4
.env.example
Normal file
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
XUI_DEBUG=true
|
||||
XUI_DB_FOLDER=x-ui
|
||||
XUI_LOG_FOLDER=x-ui
|
||||
XUI_BIN_FOLDER=x-ui
|
||||
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
@@ -11,4 +11,4 @@ issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: mhsanaei
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
custom: https://nowpayments.io/donation/hsanaei
|
||||
|
||||
87
.github/workflows/docker.yml
vendored
87
.github/workflows/docker.yml
vendored
@@ -1,4 +1,9 @@
|
||||
name: Release 3X-UI for Docker
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -7,51 +12,51 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
hsanaeii/3x-ui
|
||||
ghcr.io/mhsanaei/3x-ui
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=pep440,pattern={{version}}
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
hsanaeii/3x-ui
|
||||
ghcr.io/mhsanaei/3x-ui
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
install: true
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
165
.github/workflows/release.yml
vendored
165
.github/workflows/release.yml
vendored
@@ -7,6 +7,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
paths:
|
||||
- '**.js'
|
||||
- '**.css'
|
||||
@@ -15,7 +17,8 @@ on:
|
||||
- '**.go'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'x-ui.service'
|
||||
- 'x-ui.service.debian'
|
||||
- 'x-ui.service.rhel'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -31,73 +34,60 @@ jobs:
|
||||
- 386
|
||||
- armv5
|
||||
- s390x
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
- name: Install dependencies
|
||||
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
|
||||
- name: Build 3X-UI
|
||||
run: |
|
||||
export CGO_ENABLED=1
|
||||
export GOOS=linux
|
||||
export GOARCH=${{ matrix.platform }}
|
||||
if [ "${{ matrix.platform }}" == "arm64" ]; then
|
||||
export GOARCH=arm64
|
||||
export CC=aarch64-linux-gnu-gcc
|
||||
elif [ "${{ matrix.platform }}" == "armv7" ]; then
|
||||
export GOARCH=arm
|
||||
export GOARM=7
|
||||
export CC=arm-linux-gnueabihf-gcc
|
||||
elif [ "${{ matrix.platform }}" == "armv6" ]; then
|
||||
export GOARCH=arm
|
||||
export GOARM=6
|
||||
export CC=arm-linux-gnueabihf-gcc
|
||||
elif [ "${{ matrix.platform }}" == "386" ]; then
|
||||
export GOARCH=386
|
||||
export CC=i686-linux-gnu-gcc
|
||||
elif [ "${{ matrix.platform }}" == "armv5" ]; then
|
||||
export GOARCH=arm
|
||||
export GOARM=5
|
||||
export CC=arm-linux-gnueabi-gcc
|
||||
elif [ "${{ matrix.platform }}" == "s390x" ]; then
|
||||
export GOARCH=s390x
|
||||
export CC=s390x-linux-gnu-gcc
|
||||
fi
|
||||
go build -ldflags "-w -s" -o xui-release -v main.go
|
||||
# Use Bootlin prebuilt cross-toolchains (musl 1.2.5 in stable series)
|
||||
case "${{ matrix.platform }}" in
|
||||
amd64) BOOTLIN_ARCH="x86-64" ;;
|
||||
arm64) BOOTLIN_ARCH="aarch64" ;;
|
||||
armv7) BOOTLIN_ARCH="armv7-eabihf"; export GOARCH=arm GOARM=7 ;;
|
||||
armv6) BOOTLIN_ARCH="armv6-eabihf"; export GOARCH=arm GOARM=6 ;;
|
||||
armv5) BOOTLIN_ARCH="armv5-eabi"; export GOARCH=arm GOARM=5 ;;
|
||||
386) BOOTLIN_ARCH="x86-i686" ;;
|
||||
s390x) BOOTLIN_ARCH="s390x-z13" ;;
|
||||
esac
|
||||
echo "Resolving Bootlin musl toolchain for arch=$BOOTLIN_ARCH (platform=${{ matrix.platform }})"
|
||||
TARBALL_BASE="https://toolchains.bootlin.com/downloads/releases/toolchains/$BOOTLIN_ARCH/tarballs/"
|
||||
TARBALL_URL=$(curl -fsSL "$TARBALL_BASE" | grep -oE "${BOOTLIN_ARCH}--musl--stable-[^\"]+\\.tar\\.xz" | sort -r | head -n1)
|
||||
[ -z "$TARBALL_URL" ] && { echo "Failed to locate Bootlin musl toolchain for arch=$BOOTLIN_ARCH" >&2; exit 1; }
|
||||
echo "Downloading: $TARBALL_URL"
|
||||
cd /tmp
|
||||
curl -fL -sS -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL"
|
||||
tar -xf "$(basename "$TARBALL_URL")"
|
||||
TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1)
|
||||
export PATH="$(realpath "$TOOLCHAIN_DIR")/bin:$PATH"
|
||||
export CC=$(realpath "$(find "$TOOLCHAIN_DIR/bin" -name '*-gcc.br_real' -type f -executable | head -n1)")
|
||||
[ -z "$CC" ] && { echo "No gcc.br_real found in $TOOLCHAIN_DIR/bin" >&2; exit 1; }
|
||||
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
|
||||
cp xui-release x-ui/
|
||||
cp x-ui.service x-ui/
|
||||
cp x-ui.service.debian x-ui/
|
||||
cp x-ui.service.rhel x-ui/
|
||||
cp x-ui.sh x-ui/
|
||||
mv x-ui/xui-release x-ui/x-ui
|
||||
mkdir x-ui/bin
|
||||
cd x-ui/bin
|
||||
|
||||
# Download dependencies
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.6.8/"
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.12.8/"
|
||||
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
||||
wget -q ${Xray_URL}Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
@@ -148,10 +138,89 @@ jobs:
|
||||
|
||||
- name: Upload files to GH release
|
||||
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:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
file: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||
overwrite: 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.12.8/"
|
||||
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
4
.gitignore
vendored
@@ -29,9 +29,9 @@ main
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Ignore Go specific files
|
||||
# Ignore Go build files
|
||||
*.exe
|
||||
*.exe~
|
||||
x-ui.db
|
||||
|
||||
# Ignore Docker specific files
|
||||
docker-compose.override.yml
|
||||
|
||||
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "vscode://schemas/launch",
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run 3x-ui (Debug)",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"XUI_DEBUG": "true"
|
||||
},
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "Run 3x-ui (Debug, custom env)",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
// Set to true to serve assets/templates directly from disk for development
|
||||
"XUI_DEBUG": "true",
|
||||
// Uncomment to override DB folder location (by default uses working dir on Windows when debug)
|
||||
// "XUI_DB_FOLDER": "${workspaceFolder}",
|
||||
// Example: override log level (debug|info|notice|warn|error)
|
||||
// "XUI_LOG_LEVEL": "debug"
|
||||
},
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
75
.vscode/tasks.json
vendored
Normal file
75
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "go: build",
|
||||
"type": "shell",
|
||||
"command": "go",
|
||||
"args": [
|
||||
"build",
|
||||
"-o",
|
||||
"bin/3x-ui.exe",
|
||||
"./main.go"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$go"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "go: run",
|
||||
"type": "shell",
|
||||
"command": "go",
|
||||
"args": [
|
||||
"run",
|
||||
"./main.go"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"XUI_DEBUG": "true"
|
||||
}
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$go"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "go: test",
|
||||
"type": "shell",
|
||||
"command": "go",
|
||||
"args": [
|
||||
"test",
|
||||
"./..."
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$go"
|
||||
],
|
||||
"group": "test"
|
||||
},
|
||||
{
|
||||
"label": "go: vet",
|
||||
"type": "shell",
|
||||
"command": "go",
|
||||
"args": [
|
||||
"vet",
|
||||
"./..."
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$go"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
5
CONTRIBUTING.md
Normal file
5
CONTRIBUTING.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## Local Development Setup
|
||||
|
||||
- Create a directory named `x-ui` in the project root
|
||||
- Rename `.env.example` to `.env `
|
||||
- Run `main.go`
|
||||
@@ -27,14 +27,14 @@ case $1 in
|
||||
esac
|
||||
mkdir -p build/bin
|
||||
cd build/bin
|
||||
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.6.8/Xray-linux-${ARCH}.zip"
|
||||
curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v25.12.8/Xray-linux-${ARCH}.zip"
|
||||
unzip "Xray-linux-${ARCH}.zip"
|
||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
||||
mv xray "xray-linux-${FNAME}"
|
||||
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
wget -q -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||
wget -q -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||
wget -q -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget -q -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
curl -sfLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||
curl -sfLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||
curl -sfLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
curl -sfLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
cd ../../
|
||||
@@ -1,14 +1,14 @@
|
||||
# ========================================================
|
||||
# Stage: Builder
|
||||
# ========================================================
|
||||
FROM golang:1.24-alpine AS builder
|
||||
FROM golang:1.25-alpine AS builder
|
||||
WORKDIR /app
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apk --no-cache --update add \
|
||||
build-base \
|
||||
gcc \
|
||||
wget \
|
||||
curl \
|
||||
unzip
|
||||
|
||||
COPY . .
|
||||
@@ -49,6 +49,7 @@ RUN chmod +x \
|
||||
/usr/bin/x-ui
|
||||
|
||||
ENV XUI_ENABLE_FAIL2BAN="true"
|
||||
EXPOSE 2053
|
||||
VOLUME [ "/etc/x-ui" ]
|
||||
CMD [ "./x-ui" ]
|
||||
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
|
||||
**3X-UI** — لوحة تحكم متقدمة مفتوحة المصدر تعتمد على الويب مصممة لإدارة خادم Xray-core. توفر واجهة سهلة الاستخدام لتكوين ومراقبة بروتوكولات VPN والوكيل المختلفة.
|
||||
|
||||
@@ -41,15 +43,13 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
|
||||
**إذا كان هذا المشروع مفيدًا لك، فقد ترغب في إعطائه**:star2:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
|
||||
## النجوم عبر الزمن
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
|
||||
**3X-UI** — panel de control avanzado basado en web de código abierto diseñado para gestionar el servidor Xray-core. Ofrece una interfaz fácil de usar para configurar y monitorear varios protocolos VPN y proxy.
|
||||
|
||||
@@ -41,15 +43,14 @@ Para documentación completa, visita la [Wiki del proyecto](https://github.com/M
|
||||
|
||||
**Si este proyecto te es útil, puedes darle una**:star2:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
|
||||
## Estrellas a lo Largo del Tiempo
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
|
||||
**3X-UI** — یک پنل کنترل پیشرفته مبتنی بر وب با کد باز که برای مدیریت سرور Xray-core طراحی شده است. این پنل یک رابط کاربری آسان برای پیکربندی و نظارت بر پروتکلهای مختلف VPN و پراکسی ارائه میدهد.
|
||||
|
||||
@@ -41,15 +43,14 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
|
||||
**اگر این پروژه برای شما مفید است، میتوانید به آن یک**:star2: بدهید
|
||||
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
|
||||
## ستارهها در طول زمان
|
||||
|
||||
|
||||
29
README.md
29
README.md
@@ -7,16 +7,18 @@
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
|
||||
**3X-UI** — advanced, open-source web-based control panel designed for managing Xray-core server. It offers a user-friendly interface for configuring and monitoring various VPN and proxy protocols.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This project is only for personal using, please do not use it for illegal purposes, please do not use it in a production environment.
|
||||
> This project is only for personal usage, please do not use it for illegal purposes, and please do not use it in a production environment.
|
||||
|
||||
As an enhanced fork of the original X-UI project, 3X-UI provides improved stability, broader protocol support, and additional features.
|
||||
|
||||
@@ -41,15 +43,14 @@ For full documentation, please visit the [project Wiki](https://github.com/MHSan
|
||||
|
||||
**If this project is helpful to you, you may wish to give it a**:star2:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
|
||||
## Stargazers over Time
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
|
||||
**3X-UI** — продвинутая панель управления с открытым исходным кодом на основе веб-интерфейса, разработанная для управления сервером Xray-core. Предоставляет удобный интерфейс для настройки и мониторинга различных VPN и прокси-протоколов.
|
||||
|
||||
@@ -41,15 +43,14 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
|
||||
**Если этот проект полезен для вас, вы можете поставить ему**:star2:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
|
||||
## Звезды с течением времени
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
|
||||
**3X-UI** — 一个基于网页的高级开源控制面板,专为管理 Xray-core 服务器而设计。它提供了用户友好的界面,用于配置和监控各种 VPN 和代理协议。
|
||||
|
||||
@@ -41,15 +43,14 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
|
||||
**如果这个项目对您有帮助,您可以给它一个**:star2:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
|
||||
## 随时间变化的星标数
|
||||
|
||||
|
||||
107
config/config.go
107
config/config.go
@@ -1,9 +1,14 @@
|
||||
// Package config provides configuration management utilities for the 3x-ui panel,
|
||||
// including version information, logging levels, database paths, and environment variable handling.
|
||||
package config
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -13,24 +18,29 @@ var version string
|
||||
//go:embed name
|
||||
var name string
|
||||
|
||||
// LogLevel represents the logging level for the application.
|
||||
type LogLevel string
|
||||
|
||||
// Logging level constants
|
||||
const (
|
||||
Debug LogLevel = "debug"
|
||||
Info LogLevel = "info"
|
||||
Notice LogLevel = "notice"
|
||||
Warn LogLevel = "warn"
|
||||
Error LogLevel = "error"
|
||||
Debug LogLevel = "debug"
|
||||
Info LogLevel = "info"
|
||||
Notice LogLevel = "notice"
|
||||
Warning LogLevel = "warning"
|
||||
Error LogLevel = "error"
|
||||
)
|
||||
|
||||
// GetVersion returns the version string of the 3x-ui application.
|
||||
func GetVersion() string {
|
||||
return strings.TrimSpace(version)
|
||||
}
|
||||
|
||||
// GetName returns the name of the 3x-ui application.
|
||||
func GetName() string {
|
||||
return strings.TrimSpace(name)
|
||||
}
|
||||
|
||||
// GetLogLevel returns the current logging level based on environment variables or defaults to Info.
|
||||
func GetLogLevel() LogLevel {
|
||||
if IsDebug() {
|
||||
return Debug
|
||||
@@ -42,10 +52,12 @@ func GetLogLevel() LogLevel {
|
||||
return LogLevel(logLevel)
|
||||
}
|
||||
|
||||
// IsDebug returns true if debug mode is enabled via the XUI_DEBUG environment variable.
|
||||
func IsDebug() bool {
|
||||
return os.Getenv("XUI_DEBUG") == "true"
|
||||
}
|
||||
|
||||
// GetBinFolderPath returns the path to the binary folder, defaulting to "bin" if not set via XUI_BIN_FOLDER.
|
||||
func GetBinFolderPath() string {
|
||||
binFolderPath := os.Getenv("XUI_BIN_FOLDER")
|
||||
if binFolderPath == "" {
|
||||
@@ -54,22 +66,91 @@ func GetBinFolderPath() string {
|
||||
return binFolderPath
|
||||
}
|
||||
|
||||
func GetDBFolderPath() string {
|
||||
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
|
||||
if dbFolderPath == "" {
|
||||
dbFolderPath = "/etc/x-ui"
|
||||
func getBaseDir() string {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
return dbFolderPath
|
||||
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
|
||||
}
|
||||
|
||||
// GetDBFolderPath returns the path to the database folder based on environment variables or platform defaults.
|
||||
func GetDBFolderPath() string {
|
||||
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
|
||||
if dbFolderPath != "" {
|
||||
return dbFolderPath
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
return getBaseDir()
|
||||
}
|
||||
return "/etc/x-ui"
|
||||
}
|
||||
|
||||
// GetDBPath returns the full path to the database file.
|
||||
func GetDBPath() string {
|
||||
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
||||
}
|
||||
|
||||
// GetLogFolder returns the path to the log folder based on environment variables or platform defaults.
|
||||
func GetLogFolder() string {
|
||||
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
|
||||
if logFolderPath == "" {
|
||||
logFolderPath = "/var/log"
|
||||
if logFolderPath != "" {
|
||||
return logFolderPath
|
||||
}
|
||||
return logFolderPath
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(".", "log")
|
||||
}
|
||||
return "/var/log/x-ui"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.6.2
|
||||
2.8.6
|
||||
@@ -1,7 +1,10 @@
|
||||
// Package database provides database initialization, migration, and management utilities
|
||||
// for the 3x-ui panel using GORM with SQLite.
|
||||
package database
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
@@ -9,10 +12,10 @@ import (
|
||||
"path"
|
||||
"slices"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/database/model"
|
||||
"x-ui/util/crypto"
|
||||
"x-ui/xray"
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
@@ -45,6 +48,7 @@ func initModels() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// initUser creates a default admin user if the users table is empty.
|
||||
func initUser() error {
|
||||
empty, err := isTableEmpty("users")
|
||||
if err != nil {
|
||||
@@ -68,6 +72,7 @@ func initUser() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// runSeeders migrates user passwords to bcrypt and records seeder execution to prevent re-running.
|
||||
func runSeeders(isUsersEmpty bool) error {
|
||||
empty, err := isTableEmpty("history_of_seeders")
|
||||
if err != nil {
|
||||
@@ -107,12 +112,14 @@ func runSeeders(isUsersEmpty bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// isTableEmpty returns true if the named table contains zero rows.
|
||||
func isTableEmpty(tableName string) (bool, error) {
|
||||
var count int64
|
||||
err := db.Table(tableName).Count(&count).Error
|
||||
return count == 0, err
|
||||
}
|
||||
|
||||
// InitDB sets up the database connection, migrates models, and runs seeders.
|
||||
func InitDB(dbPath string) error {
|
||||
dir := path.Dir(dbPath)
|
||||
err := os.MkdirAll(dir, fs.ModePerm)
|
||||
@@ -141,6 +148,9 @@ func InitDB(dbPath string) error {
|
||||
}
|
||||
|
||||
isUsersEmpty, err := isTableEmpty("users")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := initUser(); err != nil {
|
||||
return err
|
||||
@@ -148,6 +158,7 @@ func InitDB(dbPath string) error {
|
||||
return runSeeders(isUsersEmpty)
|
||||
}
|
||||
|
||||
// CloseDB closes the database connection if it exists.
|
||||
func CloseDB() error {
|
||||
if db != nil {
|
||||
sqlDB, err := db.DB()
|
||||
@@ -159,14 +170,17 @@ func CloseDB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDB returns the global GORM database instance.
|
||||
func GetDB() *gorm.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
// IsNotFound checks if the given error is a GORM record not found error.
|
||||
func IsNotFound(err error) bool {
|
||||
return err == gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
// IsSQLiteDB checks if the given file is a valid SQLite database by reading its signature.
|
||||
func IsSQLiteDB(file io.ReaderAt) (bool, error) {
|
||||
signature := []byte("SQLite format 3\x00")
|
||||
buf := make([]byte, len(signature))
|
||||
@@ -177,6 +191,7 @@ func IsSQLiteDB(file io.ReaderAt) (bool, error) {
|
||||
return bytes.Equal(buf, signature), nil
|
||||
}
|
||||
|
||||
// Checkpoint performs a WAL checkpoint on the SQLite database to ensure data consistency.
|
||||
func Checkpoint() error {
|
||||
// Update WAL
|
||||
err := db.Exec("PRAGMA wal_checkpoint;").Error
|
||||
@@ -185,3 +200,29 @@ func Checkpoint() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSQLiteDB opens the provided sqlite DB path with a throw-away connection
|
||||
// and runs a PRAGMA integrity_check to ensure the file is structurally sound.
|
||||
// It does not mutate global state or run migrations.
|
||||
func ValidateSQLiteDB(dbPath string) error {
|
||||
if _, err := os.Stat(dbPath); err != nil { // file must exist
|
||||
return err
|
||||
}
|
||||
gdb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Discard})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sqlDB, err := gdb.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
var res string
|
||||
if err := gdb.Raw("PRAGMA integrity_check;").Scan(&res).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if res != "ok" {
|
||||
return errors.New("sqlite integrity check failed: " + res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,43 +1,51 @@
|
||||
// Package model defines the database models and data structures used by the 3x-ui panel.
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"x-ui/util/json_util"
|
||||
"x-ui/xray"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||
)
|
||||
|
||||
// Protocol represents the protocol type for Xray inbounds.
|
||||
type Protocol string
|
||||
|
||||
// Protocol constants for different Xray inbound protocols
|
||||
const (
|
||||
VMESS Protocol = "vmess"
|
||||
VLESS Protocol = "vless"
|
||||
DOKODEMO Protocol = "dokodemo-door"
|
||||
Tunnel Protocol = "tunnel"
|
||||
HTTP Protocol = "http"
|
||||
Trojan Protocol = "trojan"
|
||||
Shadowsocks Protocol = "shadowsocks"
|
||||
Socks Protocol = "socks"
|
||||
Mixed Protocol = "mixed"
|
||||
WireGuard Protocol = "wireguard"
|
||||
)
|
||||
|
||||
// User represents a user account in the 3x-ui panel.
|
||||
type User struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// Inbound represents an Xray inbound configuration with traffic statistics and settings.
|
||||
type Inbound struct {
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
UserId int `json:"-"`
|
||||
Up int64 `json:"up" form:"up"`
|
||||
Down int64 `json:"down" form:"down"`
|
||||
Total int64 `json:"total" form:"total"`
|
||||
Remark string `json:"remark" form:"remark"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier
|
||||
UserId int `json:"-"` // Associated user ID
|
||||
Up int64 `json:"up" form:"up"` // Upload traffic in bytes
|
||||
Down int64 `json:"down" form:"down"` // Download traffic in bytes
|
||||
Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
|
||||
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` // All-time traffic usage
|
||||
Remark string `json:"remark" form:"remark"` // Human-readable remark
|
||||
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
|
||||
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2"` // Traffic reset schedule
|
||||
LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
|
||||
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics
|
||||
|
||||
// config part
|
||||
// Xray configuration fields
|
||||
Listen string `json:"listen" form:"listen"`
|
||||
Port int `json:"port" form:"port"`
|
||||
Protocol Protocol `json:"protocol" form:"protocol"`
|
||||
@@ -45,9 +53,9 @@ type Inbound struct {
|
||||
StreamSettings string `json:"streamSettings" form:"streamSettings"`
|
||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||
Sniffing string `json:"sniffing" form:"sniffing"`
|
||||
Allocate string `json:"allocate" form:"allocate"`
|
||||
}
|
||||
|
||||
// OutboundTraffics tracks traffic statistics for Xray outbound connections.
|
||||
type OutboundTraffics struct {
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||
@@ -56,17 +64,20 @@ type OutboundTraffics struct {
|
||||
Total int64 `json:"total" form:"total" gorm:"default:0"`
|
||||
}
|
||||
|
||||
// InboundClientIps stores IP addresses associated with inbound clients for access control.
|
||||
type InboundClientIps struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
|
||||
Ips string `json:"ips" form:"ips"`
|
||||
}
|
||||
|
||||
// HistoryOfSeeders tracks which database seeders have been executed to prevent re-running.
|
||||
type HistoryOfSeeders struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
SeederName string `json:"seederName"`
|
||||
}
|
||||
|
||||
// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
|
||||
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||
listen := i.Listen
|
||||
if listen != "" {
|
||||
@@ -80,28 +91,31 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||
StreamSettings: json_util.RawMessage(i.StreamSettings),
|
||||
Tag: i.Tag,
|
||||
Sniffing: json_util.RawMessage(i.Sniffing),
|
||||
Allocate: json_util.RawMessage(i.Allocate),
|
||||
}
|
||||
}
|
||||
|
||||
// Setting stores key-value configuration settings for the 3x-ui panel.
|
||||
type Setting struct {
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Key string `json:"key" form:"key"`
|
||||
Value string `json:"value" form:"value"`
|
||||
}
|
||||
|
||||
// Client represents a client configuration for Xray inbounds with traffic limits and settings.
|
||||
type Client struct {
|
||||
ID string `json:"id"`
|
||||
Security string `json:"security"`
|
||||
Password string `json:"password"`
|
||||
Flow string `json:"flow"`
|
||||
Email string `json:"email"`
|
||||
LimitIP int `json:"limitIp"`
|
||||
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
TgID int64 `json:"tgId" form:"tgId"`
|
||||
SubID string `json:"subId" form:"subId"`
|
||||
Comment string `json:"comment" form:"comment"`
|
||||
Reset int `json:"reset" form:"reset"`
|
||||
ID string `json:"id"` // Unique client identifier
|
||||
Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm")
|
||||
Password string `json:"password"` // Client password
|
||||
Flow string `json:"flow"` // Flow control (XTLS)
|
||||
Email string `json:"email"` // Client email identifier
|
||||
LimitIP int `json:"limitIp"` // IP limit for this client
|
||||
TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
|
||||
Enable bool `json:"enable" form:"enable"` // Whether the client is enabled
|
||||
TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications
|
||||
SubID string `json:"subId" form:"subId"` // Subscription identifier
|
||||
Comment string `json:"comment" form:"comment"` // Client comment
|
||||
Reset int `json:"reset" form:"reset"` // Reset period in days
|
||||
CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
|
||||
UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
|
||||
}
|
||||
|
||||
107
go.mod
107
go.mod
@@ -1,104 +1,103 @@
|
||||
module x-ui
|
||||
module github.com/mhsanaei/3x-ui/v2
|
||||
|
||||
go 1.24.4
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/gzip v1.2.3
|
||||
github.com/gin-contrib/gzip v1.2.5
|
||||
github.com/gin-contrib/sessions v1.0.4
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mymmrac/telego v0.32.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0
|
||||
github.com/mymmrac/telego v1.4.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.6
|
||||
github.com/valyala/fasthttp v1.63.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.12
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/valyala/fasthttp v1.68.0
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
github.com/xtls/xray-core v1.250608.0
|
||||
github.com/xtls/xray-core v1.251208.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/text v0.26.0
|
||||
google.golang.org/grpc v1.73.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/text v0.32.0
|
||||
google.golang.org/grpc v1.78.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.30.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.2 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/fasthttp/router v1.5.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-yaml v1.19.1 // 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/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.4.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grbit/go-json v0.11.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/juju/ratelimit v1.0.2 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.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-20251013123823-9fd1530e3ec3 // 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.33 // indirect
|
||||
github.com/miekg/dns v1.1.69 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.52.0 // indirect
|
||||
github.com/refraction-networking/utls v1.7.3 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.58.0 // indirect
|
||||
github.com/refraction-networking/utls v1.8.1 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sagernet/sing v0.6.6 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 // indirect
|
||||
github.com/sagernet/sing v0.7.14 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/valyala/fastjson v1.6.7 // indirect
|
||||
github.com/vishvananda/netlink v1.3.1 // 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-20251116175510-cd53f7d50237 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.18.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
)
|
||||
|
||||
274
go.sum
274
go.sum
@@ -1,43 +1,47 @@
|
||||
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/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/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
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.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
|
||||
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/fasthttp/router v1.5.4 h1:oxdThbBwQgsDIYZ3wR1IavsNl6ZS9WdjKukeMikOnC8=
|
||||
github.com/fasthttp/router v1.5.4/go.mod h1:3/hysWq6cky7dTfzaaEPZGdptwjwx0qzTgFCKEWRjgc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U=
|
||||
github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c=
|
||||
github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=
|
||||
github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw=
|
||||
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
|
||||
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
|
||||
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-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
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.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
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/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
@@ -49,12 +53,12 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
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/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
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-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
||||
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
@@ -66,8 +70,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.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||
@@ -80,6 +82,20 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
|
||||
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
@@ -90,39 +106,33 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||
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/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
|
||||
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
|
||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mymmrac/telego v0.32.0 h1:4X8C1l3k+opkk86r95+eQE8DxiS2LYlR61L/G7yreDY=
|
||||
github.com/mymmrac/telego v0.32.0/go.mod h1:qS6NaRhJgcuEEBEMVCV79S2xCAuHq9O+ixwfLuRW31M=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
|
||||
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/mymmrac/telego v1.4.0 h1:z74W5lfOTgLplQXuZPjDsRvvvI0iQatO2gp/XZz7s3I=
|
||||
github.com/mymmrac/telego v1.4.0/go.mod h1:u9fKXZSOCOdMj6K0U69fQqeAvDE+2RGkHKkDksijp3o=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
@@ -135,125 +145,126 @@ 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/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/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/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
|
||||
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
||||
github.com/refraction-networking/utls v1.7.3 h1:L0WRhHY7Oq1T0zkdzVZMR6zWZv+sXbHB9zcuvsAEqCo=
|
||||
github.com/refraction-networking/utls v1.7.3/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
|
||||
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
|
||||
github.com/refraction-networking/utls v1.8.1/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/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
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.6.6/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
|
||||
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 h1:qIQ0tWF9vxGtkJa24bR+2i53WBCz1nW/Pc47oVYauC4=
|
||||
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
||||
github.com/sagernet/sing v0.7.14 h1:5QQRDCUvYNOMyVp3LuK/hYEBAIv0VsbD3x/l9zH467s=
|
||||
github.com/sagernet/sing v0.7.14/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.63.0 h1:DisIL8OjB7ul2d7cBaMRcKTQDYnrGy56R4FCiuDP0Ns=
|
||||
github.com/valyala/fasthttp v1.63.0/go.mod h1:REc4IeW+cAEyLrRPa5A81MIjvz0QE1laoTX2EaPHKJM=
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
|
||||
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
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/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
||||
github.com/xtls/reality v0.0.0-20250627141458-e62c4aed0d57 h1:CJzC54UytAYnNbJSlAFi9MXOofSUAtpoQTKIA3hUpj8=
|
||||
github.com/xtls/reality v0.0.0-20250627141458-e62c4aed0d57/go.mod h1:yD47RN65bDLZgyHWMfFDiqlzrq4usDMt/Xzsk6tMbhw=
|
||||
github.com/xtls/xray-core v1.250608.0 h1:/M0LwzFeFAZf+vdZQhqNYjUXDfPv5hOeYw6jsiAdgWI=
|
||||
github.com/xtls/xray-core v1.250608.0/go.mod h1:MkfIs2WZ5VLtZHAwDKosSS05Kx5zFFOzvly7Hy6pfPs=
|
||||
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 h1:UXjrmniKlY+ZbIqpN91lejB3pszQQQRVu1vqH/p/aGM=
|
||||
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ=
|
||||
github.com/xtls/xray-core v1.251208.0 h1:9jIXi+9KXnfmT5esSYNf9VAQlQkaAP8bG413B0eyAes=
|
||||
github.com/xtls/xray-core v1.251208.0/go.mod h1:kclzboEF0g6VBrp9/NXm8C0Aj64SDBt52OfthH1LSr4=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
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/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.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-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -265,10 +276,9 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h1:sfK5nHuG7lRFZ2FdTT3RimOqWBg8IrVm+/Vko1FVOsk=
|
||||
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
|
||||
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/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
||||
694
install.sh
694
install.sh
@@ -8,6 +8,9 @@ plain='\033[0m'
|
||||
|
||||
cur_dir=$(pwd)
|
||||
|
||||
xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}"
|
||||
xui_service="${XUI_SERVICE:=/etc/systemd/system}"
|
||||
|
||||
# check root
|
||||
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1
|
||||
|
||||
@@ -15,7 +18,7 @@ cur_dir=$(pwd)
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
source /etc/os-release
|
||||
release=$ID
|
||||
elif [[ -f /usr/lib/os-release ]]; then
|
||||
elif [[ -f /usr/lib/os-release ]]; then
|
||||
source /usr/lib/os-release
|
||||
release=$ID
|
||||
else
|
||||
@@ -26,51 +29,59 @@ echo "The OS release is: $release"
|
||||
|
||||
arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64 | x64 | amd64) echo 'amd64' ;;
|
||||
i*86 | x86) echo '386' ;;
|
||||
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
|
||||
armv7* | armv7 | arm) echo 'armv7' ;;
|
||||
armv6* | armv6) echo 'armv6' ;;
|
||||
armv5* | armv5) echo 'armv5' ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
|
||||
x86_64 | x64 | amd64) echo 'amd64' ;;
|
||||
i*86 | x86) echo '386' ;;
|
||||
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
|
||||
armv7* | armv7 | arm) echo 'armv7' ;;
|
||||
armv6* | armv6) echo 'armv6' ;;
|
||||
armv5* | armv5) echo 'armv5' ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
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+)"
|
||||
# Simple helpers
|
||||
is_ipv4() {
|
||||
[[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1
|
||||
}
|
||||
is_ipv6() {
|
||||
[[ "$1" =~ : ]] && return 0 || return 1
|
||||
}
|
||||
is_ip() {
|
||||
is_ipv4 "$1" || is_ipv6 "$1"
|
||||
}
|
||||
is_domain() {
|
||||
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1
|
||||
}
|
||||
check_glibc_version
|
||||
|
||||
install_base() {
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
apt-get update && apt-get install -y -q wget curl tar tzdata
|
||||
ubuntu | debian | armbian)
|
||||
apt-get update && apt-get install -y -q curl tar tzdata openssl socat
|
||||
;;
|
||||
centos | rhel | almalinux | rocky | ol)
|
||||
yum -y update && yum install -y -q wget curl tar tzdata
|
||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
||||
dnf -y update && dnf install -y -q curl tar tzdata openssl socat
|
||||
;;
|
||||
fedora | amzn | virtuozzo)
|
||||
dnf -y update && dnf install -y -q wget curl tar tzdata
|
||||
centos)
|
||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
||||
yum -y update && yum install -y curl tar tzdata openssl socat
|
||||
else
|
||||
dnf -y update && dnf install -y -q curl tar tzdata openssl socat
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata
|
||||
arch | manjaro | parch)
|
||||
pacman -Syu && pacman -Syu --noconfirm curl tar tzdata openssl socat
|
||||
;;
|
||||
opensuse-tumbleweed)
|
||||
zypper refresh && zypper -q install -y wget curl tar timezone
|
||||
opensuse-tumbleweed | opensuse-leap)
|
||||
zypper refresh && zypper -q install -y curl tar timezone openssl socat
|
||||
;;
|
||||
*)
|
||||
apt-get update && apt install -y -q wget curl tar tzdata
|
||||
alpine)
|
||||
apk update && apk add curl tar tzdata openssl socat
|
||||
;;
|
||||
*)
|
||||
apt-get update && apt-get install -y -q curl tar tzdata openssl socat
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -81,21 +92,395 @@ gen_random_string() {
|
||||
echo "$random_string"
|
||||
}
|
||||
|
||||
config_after_install() {
|
||||
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_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
||||
local server_ip=$(curl -s --max-time 3 https://api.ipify.org)
|
||||
if [ -z "$server_ip" ]; then
|
||||
server_ip=$(curl -s --max-time 3 https://4.ident.me)
|
||||
install_acme() {
|
||||
echo -e "${green}Installing acme.sh for SSL certificate management...${plain}"
|
||||
cd ~ || return 1
|
||||
curl -s https://get.acme.sh | sh >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Failed to install acme.sh${plain}"
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}acme.sh installed successfully${plain}"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
setup_ssl_certificate() {
|
||||
local domain="$1"
|
||||
local server_ip="$2"
|
||||
local existing_port="$3"
|
||||
local existing_webBasePath="$4"
|
||||
|
||||
echo -e "${green}Setting up SSL certificate...${plain}"
|
||||
|
||||
# Check if acme.sh is installed
|
||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||
install_acme
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create certificate directory
|
||||
local certPath="/root/cert/${domain}"
|
||||
mkdir -p "$certPath"
|
||||
|
||||
# Issue certificate
|
||||
echo -e "${green}Issuing SSL certificate for ${domain}...${plain}"
|
||||
echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
|
||||
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1
|
||||
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Failed to issue certificate for ${domain}${plain}"
|
||||
echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}"
|
||||
rm -rf ~/.acme.sh/${domain} 2>/dev/null
|
||||
rm -rf "$certPath" 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Install certificate
|
||||
~/.acme.sh/acme.sh --installcert -d ${domain} \
|
||||
--key-file /root/cert/${domain}/privkey.pem \
|
||||
--fullchain-file /root/cert/${domain}/fullchain.pem \
|
||||
--reloadcmd "systemctl restart x-ui" >/dev/null 2>&1
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Failed to install certificate${plain}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Enable auto-renew
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
|
||||
chmod 755 $certPath/* 2>/dev/null
|
||||
|
||||
# Set certificate for panel
|
||||
local webCertFile="/root/cert/${domain}/fullchain.pem"
|
||||
local webKeyFile="/root/cert/${domain}/privkey.pem"
|
||||
|
||||
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
|
||||
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1
|
||||
echo -e "${green}SSL certificate installed and configured successfully!${plain}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${yellow}Certificate files not found${plain}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Fallback: generate a self-signed certificate (not publicly trusted)
|
||||
setup_self_signed_certificate() {
|
||||
local name="$1" # domain or IP to place in SAN
|
||||
local certDir="/root/cert/selfsigned"
|
||||
|
||||
echo -e "${yellow}Generating a self-signed certificate (not publicly trusted)...${plain}"
|
||||
|
||||
mkdir -p "$certDir"
|
||||
|
||||
local sanExt=""
|
||||
if is_ip "$name"; then
|
||||
sanExt="IP:${name}"
|
||||
else
|
||||
sanExt="DNS:${name}"
|
||||
fi
|
||||
|
||||
# Use -addext if supported; fallback to config file if needed
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||
-keyout "${certDir}/privkey.pem" \
|
||||
-out "${certDir}/fullchain.pem" \
|
||||
-subj "/CN=${name}" \
|
||||
-addext "subjectAltName=${sanExt}" >/dev/null 2>&1
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
# Fallback via temporary config file (for older OpenSSL versions)
|
||||
local tmpCfg="${certDir}/openssl.cnf"
|
||||
cat > "$tmpCfg" <<EOF
|
||||
[req]
|
||||
distinguished_name=req_distinguished_name
|
||||
req_extensions=v3_req
|
||||
[req_distinguished_name]
|
||||
[v3_req]
|
||||
subjectAltName=${sanExt}
|
||||
EOF
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||
-keyout "${certDir}/privkey.pem" \
|
||||
-out "${certDir}/fullchain.pem" \
|
||||
-subj "/CN=${name}" \
|
||||
-config "$tmpCfg" -extensions v3_req >/dev/null 2>&1
|
||||
rm -f "$tmpCfg"
|
||||
fi
|
||||
|
||||
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
|
||||
echo -e "${red}Failed to generate self-signed certificate${plain}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
chmod 755 ${certDir}/* 2>/dev/null
|
||||
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1
|
||||
echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Comprehensive manual SSL certificate issuance via acme.sh
|
||||
ssl_cert_issue() {
|
||||
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')
|
||||
local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
|
||||
|
||||
# check for acme.sh first
|
||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||
echo "acme.sh could not be found. Installing now..."
|
||||
cd ~ || return 1
|
||||
curl -s https://get.acme.sh | sh
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Failed to install acme.sh${plain}"
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}acme.sh installed successfully${plain}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# get the domain here, and we need to verify it
|
||||
local domain=""
|
||||
while true; do
|
||||
read -rp "Please enter your domain name: " domain
|
||||
domain="${domain// /}" # Trim whitespace
|
||||
|
||||
if [[ -z "$domain" ]]; then
|
||||
echo -e "${red}Domain name cannot be empty. Please try again.${plain}"
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! is_domain "$domain"; then
|
||||
echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}"
|
||||
continue
|
||||
fi
|
||||
|
||||
break
|
||||
done
|
||||
echo -e "${green}Your domain is: ${domain}, checking it...${plain}"
|
||||
|
||||
# check if there already exists a certificate
|
||||
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
|
||||
if [ "${currentCert}" == "${domain}" ]; then
|
||||
local certInfo=$(~/.acme.sh/acme.sh --list)
|
||||
echo -e "${red}System already has certificates for this domain. Cannot issue again.${plain}"
|
||||
echo -e "${yellow}Current certificate details:${plain}"
|
||||
echo "$certInfo"
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}Your domain is ready for issuing certificates now...${plain}"
|
||||
fi
|
||||
|
||||
# create a directory for the certificate
|
||||
certPath="/root/cert/${domain}"
|
||||
if [ ! -d "$certPath" ]; then
|
||||
mkdir -p "$certPath"
|
||||
else
|
||||
rm -rf "$certPath"
|
||||
mkdir -p "$certPath"
|
||||
fi
|
||||
|
||||
# get the port number for the standalone server
|
||||
local WebPort=80
|
||||
read -rp "Please choose which port to use (default is 80): " WebPort
|
||||
if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then
|
||||
echo -e "${yellow}Your input ${WebPort} is invalid, will use default port 80.${plain}"
|
||||
WebPort=80
|
||||
fi
|
||||
echo -e "${green}Will use port: ${WebPort} to issue certificates. Please make sure this port is open.${plain}"
|
||||
|
||||
# Stop panel temporarily
|
||||
echo -e "${yellow}Stopping panel temporarily...${plain}"
|
||||
systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null
|
||||
|
||||
# issue the certificate
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
||||
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Issuing certificate failed, please check logs.${plain}"
|
||||
rm -rf ~/.acme.sh/${domain}
|
||||
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}"
|
||||
fi
|
||||
|
||||
# Setup reload command
|
||||
reloadCmd="systemctl restart x-ui || rc-service x-ui restart"
|
||||
echo -e "${green}Default --reloadcmd for ACME is: ${yellow}systemctl restart x-ui || rc-service x-ui restart${plain}"
|
||||
echo -e "${green}This command will run on every certificate issue and renew.${plain}"
|
||||
read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd
|
||||
if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then
|
||||
echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; systemctl restart x-ui"
|
||||
echo -e "${green}\t2.${plain} Input your own command"
|
||||
echo -e "${green}\t0.${plain} Keep default reloadcmd"
|
||||
read -rp "Choose an option: " choice
|
||||
case "$choice" in
|
||||
1)
|
||||
echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}"
|
||||
reloadCmd="systemctl reload nginx ; systemctl restart x-ui"
|
||||
;;
|
||||
2)
|
||||
echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}"
|
||||
read -rp "Please enter your custom reloadcmd: " reloadCmd
|
||||
echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}"
|
||||
;;
|
||||
*)
|
||||
echo -e "${green}Keeping default reloadcmd${plain}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# install the certificate
|
||||
~/.acme.sh/acme.sh --installcert -d ${domain} \
|
||||
--key-file /root/cert/${domain}/privkey.pem \
|
||||
--fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Installing certificate failed, exiting.${plain}"
|
||||
rm -rf ~/.acme.sh/${domain}
|
||||
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}"
|
||||
fi
|
||||
|
||||
# enable auto-renew
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}"
|
||||
ls -lah /root/cert/${domain}/
|
||||
chmod 755 $certPath/*
|
||||
else
|
||||
echo -e "${green}Auto renew succeeded, certificate details:${plain}"
|
||||
ls -lah /root/cert/${domain}/
|
||||
chmod 755 $certPath/*
|
||||
fi
|
||||
|
||||
# Restart panel
|
||||
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
|
||||
|
||||
# Prompt user to set panel paths after successful certificate installation
|
||||
read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel
|
||||
if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then
|
||||
local webCertFile="/root/cert/${domain}/fullchain.pem"
|
||||
local webKeyFile="/root/cert/${domain}/privkey.pem"
|
||||
|
||||
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
|
||||
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
|
||||
echo -e "${green}Certificate paths set for the panel${plain}"
|
||||
echo -e "${green}Certificate File: $webCertFile${plain}"
|
||||
echo -e "${green}Private Key File: $webKeyFile${plain}"
|
||||
echo ""
|
||||
echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}"
|
||||
echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}"
|
||||
systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null
|
||||
else
|
||||
echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}"
|
||||
fi
|
||||
else
|
||||
echo -e "${yellow}Skipping panel path setting.${plain}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Reusable interactive SSL setup (domain or self-signed)
|
||||
# Sets global `SSL_HOST` to the chosen domain/IP for Access URL usage
|
||||
prompt_and_setup_ssl() {
|
||||
local panel_port="$1"
|
||||
local web_base_path="$2" # expected without leading slash
|
||||
local server_ip="$3"
|
||||
|
||||
local ssl_choice=""
|
||||
|
||||
echo -e "${yellow}Choose SSL certificate setup method:${plain}"
|
||||
echo -e "${green}1.${plain} Let's Encrypt (domain required, recommended)"
|
||||
echo -e "${green}2.${plain} Self-signed certificate (not publicly trusted)"
|
||||
read -rp "Choose an option (default 2): " ssl_choice
|
||||
ssl_choice="${ssl_choice// /}" # Trim whitespace
|
||||
|
||||
# Default to 2 (self-signed) if not 1
|
||||
if [[ "$ssl_choice" != "1" ]]; then
|
||||
ssl_choice="2"
|
||||
fi
|
||||
|
||||
case "$ssl_choice" in
|
||||
1)
|
||||
# User chose Let's Encrypt domain option
|
||||
echo -e "${green}Using ssl_cert_issue() for comprehensive domain setup...${plain}"
|
||||
ssl_cert_issue
|
||||
# Extract the domain that was used from the certificate
|
||||
local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}')
|
||||
if [[ -n "${cert_domain}" ]]; then
|
||||
SSL_HOST="${cert_domain}"
|
||||
echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}"
|
||||
else
|
||||
echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}"
|
||||
SSL_HOST="${server_ip}"
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
# User chose self-signed option
|
||||
# Stop panel if running
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui stop >/dev/null 2>&1
|
||||
else
|
||||
systemctl stop x-ui >/dev/null 2>&1
|
||||
fi
|
||||
echo -e "${yellow}Using server IP for self-signed certificate: ${server_ip}${plain}"
|
||||
setup_self_signed_certificate "${server_ip}"
|
||||
if [ $? -eq 0 ]; then
|
||||
SSL_HOST="${server_ip}"
|
||||
echo -e "${green}✓ Self-signed SSL configured successfully${plain}"
|
||||
else
|
||||
echo -e "${red}✗ Self-signed SSL setup failed${plain}"
|
||||
SSL_HOST="${server_ip}"
|
||||
fi
|
||||
# Start panel after SSL is configured
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui start >/dev/null 2>&1
|
||||
else
|
||||
systemctl start x-ui >/dev/null 2>&1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
|
||||
SSL_HOST="${server_ip}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
config_after_install() {
|
||||
local existing_hasDefaultCredential=$(${xui_folder}/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}')
|
||||
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##')
|
||||
local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
||||
# Properly detect empty cert by checking if cert: line exists and has content after it
|
||||
local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
|
||||
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_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_password=$(gen_random_string 10)
|
||||
|
||||
|
||||
read -rp "Would you like to customize the Panel Port settings? (If not, a random port will be applied) [y/n]: " config_confirm
|
||||
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
|
||||
read -rp "Please set up the panel port: " config_port
|
||||
@@ -104,54 +489,105 @@ config_after_install() {
|
||||
local config_port=$(shuf -i 1024-62000 -n 1)
|
||||
echo -e "${yellow}Generated random port: ${config_port}${plain}"
|
||||
fi
|
||||
|
||||
${xui_folder}/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}"
|
||||
|
||||
echo ""
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green} SSL Certificate Setup (MANDATORY) ${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${yellow}For security, SSL certificate is required for all panels.${plain}"
|
||||
echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}"
|
||||
echo ""
|
||||
|
||||
/usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}"
|
||||
echo -e "This is a fresh installation, generating random login info for security concerns:"
|
||||
echo -e "###############################################"
|
||||
echo -e "${green}Username: ${config_username}${plain}"
|
||||
echo -e "${green}Password: ${config_password}${plain}"
|
||||
echo -e "${green}Port: ${config_port}${plain}"
|
||||
prompt_and_setup_ssl "${config_port}" "${config_webBasePath}" "${server_ip}"
|
||||
|
||||
# Display final credentials and access information
|
||||
echo ""
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green} Panel Installation Complete! ${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green}Username: ${config_username}${plain}"
|
||||
echo -e "${green}Password: ${config_password}${plain}"
|
||||
echo -e "${green}Port: ${config_port}${plain}"
|
||||
echo -e "${green}WebBasePath: ${config_webBasePath}${plain}"
|
||||
echo -e "${green}Access URL: http://${server_ip}:${config_port}/${config_webBasePath}${plain}"
|
||||
echo -e "###############################################"
|
||||
echo -e "${green}Access URL: https://${SSL_HOST}:${config_port}/${config_webBasePath}${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${yellow}⚠ IMPORTANT: Save these credentials securely!${plain}"
|
||||
echo -e "${yellow}⚠ SSL Certificate: Enabled and configured${plain}"
|
||||
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}"
|
||||
/usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}"
|
||||
${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}"
|
||||
echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
|
||||
echo -e "${green}Access URL: http://${server_ip}:${existing_port}/${config_webBasePath}${plain}"
|
||||
|
||||
# If the panel is already installed but no certificate is configured, prompt for SSL now
|
||||
if [[ -z "${existing_cert}" ]]; then
|
||||
echo ""
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}"
|
||||
echo ""
|
||||
prompt_and_setup_ssl "${existing_port}" "${config_webBasePath}" "${server_ip}"
|
||||
echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${config_webBasePath}${plain}"
|
||||
else
|
||||
# If a cert already exists, just show the access URL
|
||||
echo -e "${green}Access URL: https://${server_ip}:${existing_port}/${config_webBasePath}${plain}"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [[ "$existing_hasDefaultCredential" == "true" ]]; then
|
||||
local config_username=$(gen_random_string 10)
|
||||
local config_password=$(gen_random_string 10)
|
||||
|
||||
|
||||
echo -e "${yellow}Default credentials detected. Security update required...${plain}"
|
||||
/usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}"
|
||||
${xui_folder}/x-ui setting -username "${config_username}" -password "${config_password}"
|
||||
echo -e "Generated new random login credentials:"
|
||||
echo -e "###############################################"
|
||||
echo -e "${green}Username: ${config_username}${plain}"
|
||||
echo -e "${green}Password: ${config_password}${plain}"
|
||||
echo -e "###############################################"
|
||||
else
|
||||
echo -e "${green}Username, Password, and WebBasePath are properly set. Exiting...${plain}"
|
||||
echo -e "${green}Username, Password, and WebBasePath are properly set.${plain}"
|
||||
fi
|
||||
|
||||
# Existing install: if no cert configured, prompt user to set domain or self-signed
|
||||
# Properly detect empty cert by checking if cert: line exists and has content after it
|
||||
existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
|
||||
if [[ -z "$existing_cert" ]]; then
|
||||
echo ""
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}"
|
||||
echo ""
|
||||
prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}"
|
||||
echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}"
|
||||
else
|
||||
echo -e "${green}SSL certificate already configured. No action needed.${plain}"
|
||||
fi
|
||||
fi
|
||||
|
||||
/usr/local/x-ui/x-ui migrate
|
||||
|
||||
${xui_folder}/x-ui migrate
|
||||
}
|
||||
|
||||
install_x-ui() {
|
||||
cd /usr/local/
|
||||
|
||||
cd ${xui_folder%/x-ui}/
|
||||
|
||||
# Download resources
|
||||
if [ $# == 0 ]; then
|
||||
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
|
||||
echo -e "${red}Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later${plain}"
|
||||
exit 1
|
||||
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
|
||||
tag_version=$(curl -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$tag_version" ]]; then
|
||||
echo -e "${red}Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later${plain}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
|
||||
wget -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz
|
||||
curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}"
|
||||
exit 1
|
||||
@@ -160,47 +596,147 @@ install_x-ui() {
|
||||
tag_version=$1
|
||||
tag_version_numeric=${tag_version#v}
|
||||
min_version="2.3.5"
|
||||
|
||||
|
||||
if [[ "$(printf '%s\n' "$min_version" "$tag_version_numeric" | sort -V | head -n1)" != "$min_version" ]]; then
|
||||
echo -e "${red}Please use a newer version (at least v2.3.5). Exiting installation.${plain}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz"
|
||||
echo -e "Beginning to install x-ui $1"
|
||||
wget -N -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
|
||||
curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz ${url}
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e /usr/local/x-ui/ ]]; then
|
||||
systemctl stop x-ui
|
||||
rm /usr/local/x-ui/ -rf
|
||||
curl -4fLRo /usr/bin/x-ui-temp https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Failed to download x-ui.sh${plain}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Stop x-ui service and remove old resources
|
||||
if [[ -e ${xui_folder}/ ]]; then
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui stop
|
||||
else
|
||||
systemctl stop x-ui
|
||||
fi
|
||||
rm ${xui_folder}/ -rf
|
||||
fi
|
||||
|
||||
# Extract resources and set permissions
|
||||
tar zxvf x-ui-linux-$(arch).tar.gz
|
||||
rm x-ui-linux-$(arch).tar.gz -f
|
||||
|
||||
cd x-ui
|
||||
chmod +x x-ui
|
||||
|
||||
chmod +x x-ui.sh
|
||||
|
||||
# Check the system's architecture and rename the file accordingly
|
||||
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
|
||||
mv bin/xray-linux-$(arch) bin/xray-linux-arm
|
||||
chmod +x bin/xray-linux-arm
|
||||
fi
|
||||
|
||||
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
|
||||
chmod +x /usr/local/x-ui/x-ui.sh
|
||||
|
||||
# Update x-ui cli and se set permission
|
||||
mv -f /usr/bin/x-ui-temp /usr/bin/x-ui
|
||||
chmod +x /usr/bin/x-ui
|
||||
mkdir -p /var/log/x-ui
|
||||
config_after_install
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl start x-ui
|
||||
# Etckeeper compatibility
|
||||
if [ -d "/etc/.git" ]; then
|
||||
if [ -f "/etc/.gitignore" ]; then
|
||||
if ! grep -q "x-ui/x-ui.db" "/etc/.gitignore"; then
|
||||
echo "" >> "/etc/.gitignore"
|
||||
echo "x-ui/x-ui.db" >> "/etc/.gitignore"
|
||||
echo -e "${green}Added x-ui.db to /etc/.gitignore for etckeeper${plain}"
|
||||
fi
|
||||
else
|
||||
echo "x-ui/x-ui.db" > "/etc/.gitignore"
|
||||
echo -e "${green}Created /etc/.gitignore and added x-ui.db for etckeeper${plain}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $release == "alpine" ]]; then
|
||||
curl -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Failed to download x-ui.rc${plain}"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x /etc/init.d/x-ui
|
||||
rc-update add x-ui
|
||||
rc-service x-ui start
|
||||
else
|
||||
# Install systemd service file
|
||||
service_installed=false
|
||||
|
||||
if [ -f "x-ui.service" ]; then
|
||||
echo -e "${green}Found x-ui.service in extracted files, installing...${plain}"
|
||||
cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1
|
||||
if [[ $? -eq 0 ]]; then
|
||||
service_installed=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$service_installed" = false ]; then
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
if [ -f "x-ui.service.debian" ]; then
|
||||
echo -e "${green}Found x-ui.service.debian in extracted files, installing...${plain}"
|
||||
cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
if [[ $? -eq 0 ]]; then
|
||||
service_installed=true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [ -f "x-ui.service.rhel" ]; then
|
||||
echo -e "${green}Found x-ui.service.rhel in extracted files, installing...${plain}"
|
||||
cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
if [[ $? -eq 0 ]]; then
|
||||
service_installed=true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# If service file not found in tar.gz, download from GitHub
|
||||
if [ "$service_installed" = false ]; then
|
||||
echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}"
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Failed to install x-ui.service from GitHub${plain}"
|
||||
exit 1
|
||||
fi
|
||||
service_installed=true
|
||||
fi
|
||||
|
||||
if [ "$service_installed" = true ]; then
|
||||
echo -e "${green}Setting up systemd unit...${plain}"
|
||||
chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
chmod 644 ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl start x-ui
|
||||
else
|
||||
echo -e "${red}Failed to install x-ui.service file${plain}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
|
||||
echo -e ""
|
||||
echo -e "┌───────────────────────────────────────────────────────┐
|
||||
@@ -217,7 +753,7 @@ install_x-ui() {
|
||||
│ ${blue}x-ui log${plain} - Check logs │
|
||||
│ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
|
||||
│ ${blue}x-ui update${plain} - Update │
|
||||
│ ${blue}x-ui legacy${plain} - legacy version │
|
||||
│ ${blue}x-ui legacy${plain} - Legacy version │
|
||||
│ ${blue}x-ui install${plain} - Install │
|
||||
│ ${blue}x-ui uninstall${plain} - Uninstall │
|
||||
└───────────────────────────────────────────────────────┘"
|
||||
|
||||
138
logger/logger.go
138
logger/logger.go
@@ -1,15 +1,29 @@
|
||||
// Package logger provides logging functionality for the 3x-ui panel with
|
||||
// dual-backend logging (console/syslog and file) and buffered log storage for web UI.
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
const (
|
||||
maxLogBufferSize = 10240 // Maximum log entries kept in memory
|
||||
logFileName = "3xui.log" // Log file name
|
||||
timeFormat = "2006/01/02 15:04:05" // Log timestamp format
|
||||
)
|
||||
|
||||
var (
|
||||
logger *logging.Logger
|
||||
logger *logging.Logger
|
||||
logFile *os.File
|
||||
|
||||
// logBuffer maintains recent log entries in memory for web UI retrieval
|
||||
logBuffer []struct {
|
||||
time string
|
||||
level logging.Level
|
||||
@@ -17,89 +31,164 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
InitLogger(logging.INFO)
|
||||
}
|
||||
|
||||
// InitLogger initializes dual logging backends: console/syslog and file.
|
||||
// Console logging uses the specified level, file logging always uses DEBUG level.
|
||||
func InitLogger(level logging.Level) {
|
||||
newLogger := logging.MustGetLogger("x-ui")
|
||||
var err error
|
||||
var backend logging.Backend
|
||||
var format logging.Formatter
|
||||
ppid := os.Getppid()
|
||||
backends := make([]logging.Backend, 0, 2)
|
||||
|
||||
backend, err = logging.NewSyslogBackend("")
|
||||
if err != nil {
|
||||
println(err)
|
||||
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
||||
}
|
||||
if ppid > 0 && err != nil {
|
||||
format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`)
|
||||
} else {
|
||||
format = logging.MustStringFormatter(`%{level} - %{message}`)
|
||||
// Console/syslog backend with configurable level
|
||||
if consoleBackend := initDefaultBackend(); consoleBackend != nil {
|
||||
leveledBackend := logging.AddModuleLevel(consoleBackend)
|
||||
leveledBackend.SetLevel(level, "x-ui")
|
||||
backends = append(backends, leveledBackend)
|
||||
}
|
||||
|
||||
backendFormatter := logging.NewBackendFormatter(backend, format)
|
||||
backendLeveled := logging.AddModuleLevel(backendFormatter)
|
||||
backendLeveled.SetLevel(level, "x-ui")
|
||||
newLogger.SetBackend(backendLeveled)
|
||||
// File backend with DEBUG level for comprehensive logging
|
||||
if fileBackend := initFileBackend(); fileBackend != nil {
|
||||
leveledBackend := logging.AddModuleLevel(fileBackend)
|
||||
leveledBackend.SetLevel(logging.DEBUG, "x-ui")
|
||||
backends = append(backends, leveledBackend)
|
||||
}
|
||||
|
||||
multiBackend := logging.MultiLogger(backends...)
|
||||
newLogger.SetBackend(multiBackend)
|
||||
logger = newLogger
|
||||
}
|
||||
|
||||
// initDefaultBackend creates the console/syslog logging backend.
|
||||
// Windows: Uses stderr directly (no syslog support)
|
||||
// Unix-like: Attempts syslog, falls back to stderr
|
||||
func initDefaultBackend() logging.Backend {
|
||||
var backend logging.Backend
|
||||
includeTime := false
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows: Use stderr directly (no syslog support)
|
||||
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
||||
includeTime = true
|
||||
} else {
|
||||
// Unix-like: Try syslog, fallback to stderr
|
||||
if syslogBackend, err := logging.NewSyslogBackend(""); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "syslog backend disabled: %v\n", err)
|
||||
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
||||
includeTime = os.Getppid() > 0
|
||||
} else {
|
||||
backend = syslogBackend
|
||||
}
|
||||
}
|
||||
|
||||
return logging.NewBackendFormatter(backend, newFormatter(includeTime))
|
||||
}
|
||||
|
||||
// initFileBackend creates the file logging backend.
|
||||
// Creates log directory and truncates log file on startup for fresh logs.
|
||||
func initFileBackend() logging.Backend {
|
||||
logDir := config.GetLogFolder()
|
||||
if err := os.MkdirAll(logDir, 0o750); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to create log folder %s: %v\n", logDir, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
logPath := filepath.Join(logDir, logFileName)
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o660)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to open log file %s: %v\n", logPath, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close previous log file if exists
|
||||
if logFile != nil {
|
||||
_ = logFile.Close()
|
||||
}
|
||||
logFile = file
|
||||
|
||||
backend := logging.NewLogBackend(file, "", 0)
|
||||
return logging.NewBackendFormatter(backend, newFormatter(true))
|
||||
}
|
||||
|
||||
// newFormatter creates a log formatter with optional timestamp.
|
||||
func newFormatter(withTime bool) logging.Formatter {
|
||||
format := `%{level} - %{message}`
|
||||
if withTime {
|
||||
format = `%{time:` + timeFormat + `} %{level} - %{message}`
|
||||
}
|
||||
return logging.MustStringFormatter(format)
|
||||
}
|
||||
|
||||
// CloseLogger closes the log file and cleans up resources.
|
||||
// Should be called during application shutdown.
|
||||
func CloseLogger() {
|
||||
if logFile != nil {
|
||||
_ = logFile.Close()
|
||||
logFile = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Debug logs a debug message and adds it to the log buffer.
|
||||
func Debug(args ...any) {
|
||||
logger.Debug(args...)
|
||||
addToBuffer("DEBUG", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Debugf logs a formatted debug message and adds it to the log buffer.
|
||||
func Debugf(format string, args ...any) {
|
||||
logger.Debugf(format, args...)
|
||||
addToBuffer("DEBUG", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Info logs an info message and adds it to the log buffer.
|
||||
func Info(args ...any) {
|
||||
logger.Info(args...)
|
||||
addToBuffer("INFO", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Infof logs a formatted info message and adds it to the log buffer.
|
||||
func Infof(format string, args ...any) {
|
||||
logger.Infof(format, args...)
|
||||
addToBuffer("INFO", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Notice logs a notice message and adds it to the log buffer.
|
||||
func Notice(args ...any) {
|
||||
logger.Notice(args...)
|
||||
addToBuffer("NOTICE", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Noticef logs a formatted notice message and adds it to the log buffer.
|
||||
func Noticef(format string, args ...any) {
|
||||
logger.Noticef(format, args...)
|
||||
addToBuffer("NOTICE", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Warning logs a warning message and adds it to the log buffer.
|
||||
func Warning(args ...any) {
|
||||
logger.Warning(args...)
|
||||
addToBuffer("WARNING", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Warningf logs a formatted warning message and adds it to the log buffer.
|
||||
func Warningf(format string, args ...any) {
|
||||
logger.Warningf(format, args...)
|
||||
addToBuffer("WARNING", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Error logs an error message and adds it to the log buffer.
|
||||
func Error(args ...any) {
|
||||
logger.Error(args...)
|
||||
addToBuffer("ERROR", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Errorf logs a formatted error message and adds it to the log buffer.
|
||||
func Errorf(format string, args ...any) {
|
||||
logger.Errorf(format, args...)
|
||||
addToBuffer("ERROR", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// addToBuffer adds a log entry to the in-memory ring buffer for web UI retrieval.
|
||||
func addToBuffer(level string, newLog string) {
|
||||
t := time.Now()
|
||||
if len(logBuffer) >= 10240 {
|
||||
if len(logBuffer) >= maxLogBufferSize {
|
||||
logBuffer = logBuffer[1:]
|
||||
}
|
||||
|
||||
@@ -109,12 +198,13 @@ func addToBuffer(level string, newLog string) {
|
||||
level logging.Level
|
||||
log string
|
||||
}{
|
||||
time: t.Format("2006/01/02 15:04:05"),
|
||||
time: t.Format(timeFormat),
|
||||
level: logLevel,
|
||||
log: newLog,
|
||||
})
|
||||
}
|
||||
|
||||
// GetLogs retrieves up to c log entries from the buffer that are at or below the specified level.
|
||||
func GetLogs(c int, level string) []string {
|
||||
var output []string
|
||||
logLevel, _ := logging.LogLevel(level)
|
||||
|
||||
54
main.go
54
main.go
@@ -1,3 +1,5 @@
|
||||
// Package main is the entry point for the 3x-ui web panel application.
|
||||
// It initializes the database, web server, and handles command-line operations for managing the panel.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -9,19 +11,20 @@ import (
|
||||
"syscall"
|
||||
_ "unsafe"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
"x-ui/logger"
|
||||
"x-ui/sub"
|
||||
"x-ui/util/crypto"
|
||||
"x-ui/web"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
"github.com/mhsanaei/3x-ui/v2/database"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/sub"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||
"github.com/mhsanaei/3x-ui/v2/web"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/global"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
// runWebServer initializes and starts the web server for the 3x-ui panel.
|
||||
func runWebServer() {
|
||||
log.Printf("Starting %v %v", config.GetName(), config.GetVersion())
|
||||
|
||||
@@ -32,7 +35,7 @@ func runWebServer() {
|
||||
logger.InitLogger(logging.INFO)
|
||||
case config.Notice:
|
||||
logger.InitLogger(logging.NOTICE)
|
||||
case config.Warn:
|
||||
case config.Warning:
|
||||
logger.InitLogger(logging.WARNING)
|
||||
case config.Error:
|
||||
logger.InitLogger(logging.ERROR)
|
||||
@@ -75,6 +78,10 @@ func runWebServer() {
|
||||
case syscall.SIGHUP:
|
||||
logger.Info("Received SIGHUP signal. Restarting servers...")
|
||||
|
||||
// --- FIX FOR TELEGRAM BOT CONFLICT (409): Stop bot before restart ---
|
||||
service.StopBot()
|
||||
// --
|
||||
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
logger.Debug("Error stopping web server:", err)
|
||||
@@ -103,6 +110,10 @@ func runWebServer() {
|
||||
log.Println("Sub server restarted successfully.")
|
||||
|
||||
default:
|
||||
// --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown ---
|
||||
service.StopBot()
|
||||
// ------------------------------------------------------------
|
||||
|
||||
server.Stop()
|
||||
subServer.Stop()
|
||||
log.Println("Shutting down servers.")
|
||||
@@ -111,6 +122,7 @@ func runWebServer() {
|
||||
}
|
||||
}
|
||||
|
||||
// resetSetting resets all panel settings to their default values.
|
||||
func resetSetting() {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
@@ -127,6 +139,7 @@ func resetSetting() {
|
||||
}
|
||||
}
|
||||
|
||||
// showSetting displays the current panel settings if show is true.
|
||||
func showSetting(show bool) {
|
||||
if show {
|
||||
settingService := service.SettingService{}
|
||||
@@ -176,6 +189,7 @@ func showSetting(show bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// updateTgbotEnableSts enables or disables the Telegram bot notifications based on the status parameter.
|
||||
func updateTgbotEnableSts(status bool) {
|
||||
settingService := service.SettingService{}
|
||||
currentTgSts, err := settingService.GetTgbotEnabled()
|
||||
@@ -195,6 +209,7 @@ func updateTgbotEnableSts(status bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// updateTgbotSetting updates Telegram bot settings including token, chat ID, and runtime schedule.
|
||||
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
@@ -232,6 +247,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
|
||||
}
|
||||
}
|
||||
|
||||
// updateSetting updates various panel settings including port, credentials, base path, listen IP, and two-factor authentication.
|
||||
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
@@ -290,6 +306,7 @@ func updateSetting(port int, username string, password string, webBasePath strin
|
||||
}
|
||||
}
|
||||
|
||||
// updateCert updates the SSL certificate files for the panel.
|
||||
func updateCert(publicKey string, privateKey string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
@@ -312,11 +329,26 @@ func updateCert(publicKey string, privateKey string) {
|
||||
} else {
|
||||
fmt.Println("set certificate private key success")
|
||||
}
|
||||
|
||||
err = settingService.SetSubCertFile(publicKey)
|
||||
if err != nil {
|
||||
fmt.Println("set certificate for subscription public key failed:", err)
|
||||
} else {
|
||||
fmt.Println("set certificate for subscription public key success")
|
||||
}
|
||||
|
||||
err = settingService.SetSubKeyFile(privateKey)
|
||||
if err != nil {
|
||||
fmt.Println("set certificate for subscription private key failed:", err)
|
||||
} else {
|
||||
fmt.Println("set certificate for subscription private key success")
|
||||
}
|
||||
} else {
|
||||
fmt.Println("both public and private key should be entered.")
|
||||
}
|
||||
}
|
||||
|
||||
// GetCertificate displays the current SSL certificate settings if getCert is true.
|
||||
func GetCertificate(getCert bool) {
|
||||
if getCert {
|
||||
settingService := service.SettingService{}
|
||||
@@ -334,6 +366,7 @@ func GetCertificate(getCert bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetListenIP displays the current panel listen IP address if getListen is true.
|
||||
func GetListenIP(getListen bool) {
|
||||
if getListen {
|
||||
|
||||
@@ -348,6 +381,7 @@ func GetListenIP(getListen bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// migrateDb performs database migration operations for the 3x-ui panel.
|
||||
func migrateDb() {
|
||||
inboundService := service.InboundService{}
|
||||
|
||||
@@ -360,6 +394,8 @@ func migrateDb() {
|
||||
fmt.Println("Migration done!")
|
||||
}
|
||||
|
||||
// main is the entry point of the 3x-ui application.
|
||||
// It parses command-line arguments to run the web server, migrate database, or update settings.
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
runWebServer()
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.1 KiB |
BIN
media/default-yellow.png
Normal file
BIN
media/default-yellow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
1
media/donation-button-black.svg
Normal file
1
media/donation-button-black.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 10 KiB |
@@ -13,7 +13,7 @@
|
||||
"inbounds": [
|
||||
{
|
||||
"port": 10808,
|
||||
"protocol": "socks",
|
||||
"protocol": "mixed",
|
||||
"settings": {
|
||||
"auth": "noauth",
|
||||
"udp": true,
|
||||
@@ -28,7 +28,7 @@
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
"tag": "socks"
|
||||
"tag": "mixed"
|
||||
},
|
||||
{
|
||||
"port": 10809,
|
||||
|
||||
170
sub/sub.go
170
sub/sub.go
@@ -1,23 +1,47 @@
|
||||
// Package sub provides subscription server functionality for the 3x-ui panel,
|
||||
// including HTTP/HTTPS servers for serving subscription links and JSON configurations.
|
||||
package sub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/middleware"
|
||||
"x-ui/web/network"
|
||||
"x-ui/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||
webpkg "github.com/mhsanaei/3x-ui/v2/web"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/locale"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/middleware"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/network"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
|
||||
"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/settings/panel/subscription/subpage.html",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
engine.SetHTMLTemplate(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Server represents the subscription server that serves subscription links and JSON configurations.
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
@@ -29,6 +53,7 @@ type Server struct {
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// NewServer creates a new subscription server instance with a cancellable context.
|
||||
func NewServer() *Server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Server{
|
||||
@@ -37,14 +62,13 @@ func NewServer() *Server {
|
||||
}
|
||||
}
|
||||
|
||||
// initRouter configures the subscription server's Gin engine, middleware,
|
||||
// templates and static assets and returns the ready-to-use engine.
|
||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
if config.IsDebug() {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
gin.DefaultWriter = io.Discard
|
||||
gin.DefaultErrorWriter = io.Discard
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
// Always run in release mode for the subscription server
|
||||
gin.DefaultWriter = io.Discard
|
||||
gin.DefaultErrorWriter = io.Discard
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
engine := gin.Default()
|
||||
|
||||
@@ -67,6 +91,23 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Determine if JSON subscription endpoint is enabled
|
||||
subJsonEnable, err := s.settingService.GetSubJsonEnable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set base_path based on LinksPath for template rendering
|
||||
// Ensure LinksPath ends with "/" for proper asset URL generation
|
||||
basePath := LinksPath
|
||||
if basePath != "/" && !strings.HasSuffix(basePath, "/") {
|
||||
basePath += "/"
|
||||
}
|
||||
// logger.Debug("sub: Setting base_path to:", basePath)
|
||||
engine.Use(func(c *gin.Context) {
|
||||
c.Set("base_path", basePath)
|
||||
})
|
||||
|
||||
Encrypt, err := s.settingService.GetSubEncrypt()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -112,15 +153,114 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
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
|
||||
// Serve under both root (/assets) and under the subscription path prefix (LinksPath + "assets")
|
||||
// so reverse proxies with a URI prefix can load assets correctly.
|
||||
// Determine LinksPath earlier to compute prefixed assets mount.
|
||||
// Note: LinksPath always starts and ends with "/" (validated in settings).
|
||||
var linksPathForAssets string
|
||||
if LinksPath == "/" {
|
||||
linksPathForAssets = "/assets"
|
||||
} else {
|
||||
// ensure single slash join
|
||||
linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets"
|
||||
}
|
||||
|
||||
// Mount assets in multiple paths to handle different URL patterns
|
||||
var assetsFS http.FileSystem
|
||||
if _, err := os.Stat("web/assets"); err == nil {
|
||||
assetsFS = http.FS(os.DirFS("web/assets"))
|
||||
} else {
|
||||
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
|
||||
assetsFS = http.FS(subFS)
|
||||
} else {
|
||||
logger.Error("sub: failed to mount embedded assets:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if assetsFS != nil {
|
||||
engine.StaticFS("/assets", assetsFS)
|
||||
if linksPathForAssets != "/assets" {
|
||||
engine.StaticFS(linksPathForAssets, assetsFS)
|
||||
}
|
||||
|
||||
// Add middleware to handle dynamic asset paths with subid
|
||||
if LinksPath != "/" {
|
||||
engine.Use(func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
// Check if this is an asset request with subid pattern: /sub/path/{subid}/assets/...
|
||||
pathPrefix := strings.TrimRight(LinksPath, "/") + "/"
|
||||
if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") {
|
||||
// Extract the asset path after /assets/
|
||||
assetsIndex := strings.Index(path, "/assets/")
|
||||
if assetsIndex != -1 {
|
||||
assetPath := path[assetsIndex+8:] // +8 to skip "/assets/"
|
||||
if assetPath != "" {
|
||||
// Serve the asset file
|
||||
c.FileFromFS(assetPath, assetsFS)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
g := engine.Group("/")
|
||||
|
||||
s.sub = NewSUBController(
|
||||
g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||
g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle)
|
||||
|
||||
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", "subpage.html")
|
||||
if _, err := os.Stat(page); err == nil {
|
||||
files = append(files, page)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// Start initializes and starts the subscription server with configured settings.
|
||||
func (s *Server) Start() (err error) {
|
||||
// This is an anonymous function, no function name
|
||||
defer func() {
|
||||
@@ -194,6 +334,7 @@ func (s *Server) Start() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down the subscription server and closes the listener.
|
||||
func (s *Server) Stop() error {
|
||||
s.cancel()
|
||||
|
||||
@@ -208,6 +349,7 @@ func (s *Server) Stop() error {
|
||||
return common.Combine(err1, err2)
|
||||
}
|
||||
|
||||
// GetCtx returns the server's context for cancellation and deadline management.
|
||||
func (s *Server) GetCtx() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
|
||||
@@ -2,16 +2,20 @@ package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SUBController handles HTTP requests for subscription links and JSON configurations.
|
||||
type SUBController struct {
|
||||
subTitle string
|
||||
subPath string
|
||||
subJsonPath string
|
||||
jsonEnabled bool
|
||||
subEncrypt bool
|
||||
updateInterval string
|
||||
|
||||
@@ -19,10 +23,12 @@ type SUBController struct {
|
||||
subJsonService *SubJsonService
|
||||
}
|
||||
|
||||
// NewSUBController creates a new subscription controller with the given configuration.
|
||||
func NewSUBController(
|
||||
g *gin.RouterGroup,
|
||||
subPath string,
|
||||
jsonPath string,
|
||||
jsonEnabled bool,
|
||||
encrypt bool,
|
||||
showInfo bool,
|
||||
rModel string,
|
||||
@@ -38,6 +44,7 @@ func NewSUBController(
|
||||
subTitle: subTitle,
|
||||
subPath: subPath,
|
||||
subJsonPath: jsonPath,
|
||||
jsonEnabled: jsonEnabled,
|
||||
subEncrypt: encrypt,
|
||||
updateInterval: update,
|
||||
|
||||
@@ -48,32 +55,22 @@ func NewSUBController(
|
||||
return a
|
||||
}
|
||||
|
||||
// initRouter registers HTTP routes for subscription links and JSON endpoints
|
||||
// on the provided router group.
|
||||
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||
gLink := g.Group(a.subPath)
|
||||
gJson := g.Group(a.subJsonPath)
|
||||
|
||||
gLink.GET(":subid", a.subs)
|
||||
|
||||
gJson.GET(":subid", a.subJsons)
|
||||
if a.jsonEnabled {
|
||||
gJson := g.Group(a.subJsonPath)
|
||||
gJson.GET(":subid", a.subJsons)
|
||||
}
|
||||
}
|
||||
|
||||
// subs handles HTTP requests for subscription links, returning either HTML page or base64-encoded subscription data.
|
||||
func (a *SUBController) subs(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
var host string
|
||||
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
|
||||
}
|
||||
}
|
||||
subs, header, err := a.subService.GetSubs(subId, host)
|
||||
scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c)
|
||||
subs, lastOnline, traffic, err := a.subService.GetSubs(subId, host)
|
||||
if err != nil || len(subs) == 0 {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
@@ -82,10 +79,55 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||
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)
|
||||
if !a.jsonEnabled {
|
||||
subJsonURL = ""
|
||||
}
|
||||
// Get base_path from context (set by middleware)
|
||||
basePath, exists := c.Get("base_path")
|
||||
if !exists {
|
||||
basePath = "/"
|
||||
}
|
||||
// Add subId to base_path for asset URLs
|
||||
basePathStr := basePath.(string)
|
||||
if basePathStr == "/" {
|
||||
basePathStr = "/" + subId + "/"
|
||||
} else {
|
||||
// Remove trailing slash if exists, add subId, then add trailing slash
|
||||
basePathStr = strings.TrimRight(basePathStr, "/") + "/" + subId + "/"
|
||||
}
|
||||
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL, basePathStr)
|
||||
c.HTML(200, "subpage.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
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
|
||||
header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
||||
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
|
||||
|
||||
if a.subEncrypt {
|
||||
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||
@@ -95,43 +137,25 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// subJsons handles HTTP requests for JSON subscription configurations.
|
||||
func (a *SUBController) subJsons(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
var host string
|
||||
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
|
||||
}
|
||||
}
|
||||
_, host, _, _ := a.subService.ResolveRequest(c)
|
||||
jsonSub, header, err := a.subJsonService.GetJson(subId, host)
|
||||
if err != nil || len(jsonSub) == 0 {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
|
||||
// Add headers
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
|
||||
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
|
||||
|
||||
c.String(200, jsonSub)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title.
|
||||
func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle)))
|
||||
}
|
||||
|
||||
@@ -6,17 +6,18 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/json_util"
|
||||
"x-ui/util/random"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/random"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||
)
|
||||
|
||||
//go:embed default.json
|
||||
var defaultJson string
|
||||
|
||||
// SubJsonService handles JSON subscription configuration generation and management.
|
||||
type SubJsonService struct {
|
||||
configJson map[string]any
|
||||
defaultOutbounds []json_util.RawMessage
|
||||
@@ -28,6 +29,7 @@ type SubJsonService struct {
|
||||
SubService *SubService
|
||||
}
|
||||
|
||||
// NewSubJsonService creates a new JSON subscription service with the given configuration.
|
||||
func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService {
|
||||
var configJson map[string]any
|
||||
var defaultOutbounds []json_util.RawMessage
|
||||
@@ -67,6 +69,7 @@ func NewSubJsonService(fragment string, noises string, mux string, rules string,
|
||||
}
|
||||
}
|
||||
|
||||
// GetJson generates a JSON subscription configuration for the given subscription ID and host.
|
||||
func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
|
||||
inbounds, err := s.SubService.getInboundsBySubId(subId)
|
||||
if err != nil || len(inbounds) == 0 {
|
||||
@@ -171,12 +174,12 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
||||
case "tls":
|
||||
if newStream["security"] != "tls" {
|
||||
newStream["security"] = "tls"
|
||||
newStream["tslSettings"] = map[string]any{}
|
||||
newStream["tlsSettings"] = map[string]any{}
|
||||
}
|
||||
case "none":
|
||||
if newStream["security"] != "none" {
|
||||
newStream["security"] = "none"
|
||||
delete(newStream, "tslSettings")
|
||||
delete(newStream, "tlsSettings")
|
||||
}
|
||||
}
|
||||
streamSettings, _ := json.MarshalIndent(newStream, "", " ")
|
||||
@@ -184,8 +187,10 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
||||
var newOutbounds []json_util.RawMessage
|
||||
|
||||
switch inbound.Protocol {
|
||||
case "vmess", "vless":
|
||||
case "vmess":
|
||||
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client))
|
||||
case "vless":
|
||||
newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client))
|
||||
case "trojan", "shadowsocks":
|
||||
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
|
||||
}
|
||||
@@ -209,9 +214,10 @@ func (s *SubJsonService) streamData(stream string) map[string]any {
|
||||
var streamSettings map[string]any
|
||||
json.Unmarshal([]byte(stream), &streamSettings)
|
||||
security, _ := streamSettings["security"].(string)
|
||||
if security == "tls" {
|
||||
switch security {
|
||||
case "tls":
|
||||
streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any))
|
||||
} else if security == "reality" {
|
||||
case "reality":
|
||||
streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any))
|
||||
}
|
||||
delete(streamSettings, "sockopt")
|
||||
@@ -263,6 +269,7 @@ func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
|
||||
rltyData["show"] = false
|
||||
rltyData["publicKey"] = rltyClientSettings["publicKey"]
|
||||
rltyData["fingerprint"] = rltyClientSettings["fingerprint"]
|
||||
rltyData["mldsa65Verify"] = rltyClientSettings["mldsa65Verify"]
|
||||
|
||||
// Set random data
|
||||
rltyData["spiderX"] = "/" + random.Seq(15)
|
||||
@@ -287,15 +294,8 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
|
||||
usersData := make([]UserVnext, 1)
|
||||
|
||||
usersData[0].ID = client.ID
|
||||
usersData[0].Level = 8
|
||||
if inbound.Protocol == model.VMESS {
|
||||
usersData[0].Security = client.Security
|
||||
}
|
||||
if inbound.Protocol == model.VLESS {
|
||||
usersData[0].Flow = client.Flow
|
||||
usersData[0].Encryption = "none"
|
||||
}
|
||||
|
||||
usersData[0].Email = client.Email
|
||||
usersData[0].Security = client.Security
|
||||
vnextData := make([]VnextSetting, 1)
|
||||
vnextData[0] = VnextSetting{
|
||||
Address: inbound.Listen,
|
||||
@@ -309,14 +309,42 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
|
||||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
outbound.Settings = OutboundSettings{
|
||||
Vnext: vnextData,
|
||||
outbound.Settings = map[string]any{
|
||||
"vnext": vnextData,
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||
outbound := Outbound{}
|
||||
outbound.Protocol = string(inbound.Protocol)
|
||||
outbound.Tag = "proxy"
|
||||
if s.mux != "" {
|
||||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
settings := make(map[string]any)
|
||||
settings["address"] = inbound.Listen
|
||||
settings["port"] = inbound.Port
|
||||
settings["id"] = client.ID
|
||||
if client.Flow != "" {
|
||||
settings["flow"] = client.Flow
|
||||
}
|
||||
|
||||
// Add encryption for VLESS outbound from inbound settings
|
||||
var inboundSettings map[string]any
|
||||
json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
|
||||
if encryption, ok := inboundSettings["encryption"].(string); ok {
|
||||
settings["encryption"] = encryption
|
||||
}
|
||||
|
||||
outbound.Settings = settings
|
||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||
outbound := Outbound{}
|
||||
|
||||
@@ -348,8 +376,8 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
|
||||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
outbound.Settings = OutboundSettings{
|
||||
Servers: serverData,
|
||||
outbound.Settings = map[string]any{
|
||||
"servers": serverData,
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||
@@ -361,13 +389,7 @@ type Outbound struct {
|
||||
Tag string `json:"tag"`
|
||||
StreamSettings json_util.RawMessage `json:"streamSettings"`
|
||||
Mux json_util.RawMessage `json:"mux,omitempty"`
|
||||
ProxySettings map[string]any `json:"proxySettings,omitempty"`
|
||||
Settings OutboundSettings `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
type OutboundSettings struct {
|
||||
Vnext []VnextSetting `json:"vnext,omitempty"`
|
||||
Servers []ServerSetting `json:"servers,omitempty"`
|
||||
Settings map[string]any `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
type VnextSetting struct {
|
||||
@@ -377,11 +399,9 @@ type VnextSetting struct {
|
||||
}
|
||||
|
||||
type UserVnext struct {
|
||||
Encryption string `json:"encryption,omitempty"`
|
||||
Flow string `json:"flow,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Security string `json:"security,omitempty"`
|
||||
Level int `json:"level"`
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Security string `json:"security,omitempty"`
|
||||
}
|
||||
|
||||
type ServerSetting struct {
|
||||
|
||||
@@ -3,21 +3,24 @@ package sub
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/util/random"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/goccy/go-json"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/database"
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/random"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||
)
|
||||
|
||||
// SubService provides business logic for generating subscription links and managing subscription data.
|
||||
type SubService struct {
|
||||
address string
|
||||
showInfo bool
|
||||
@@ -27,6 +30,7 @@ type SubService struct {
|
||||
settingService service.SettingService
|
||||
}
|
||||
|
||||
// NewSubService creates a new subscription service with the given configuration.
|
||||
func NewSubService(showInfo bool, remarkModel string) *SubService {
|
||||
return &SubService{
|
||||
showInfo: showInfo,
|
||||
@@ -34,19 +38,20 @@ func NewSubService(showInfo bool, remarkModel string) *SubService {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
|
||||
// GetSubs retrieves subscription links for a given subscription ID and host.
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.ClientTraffic, error) {
|
||||
s.address = host
|
||||
var result []string
|
||||
var header string
|
||||
var traffic xray.ClientTraffic
|
||||
var lastOnline int64
|
||||
var clientTraffics []xray.ClientTraffic
|
||||
inbounds, err := s.getInboundsBySubId(subId)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, 0, traffic, err
|
||||
}
|
||||
|
||||
if len(inbounds) == 0 {
|
||||
return nil, "", common.NewError("No inbounds found with ", subId)
|
||||
return nil, 0, traffic, common.NewError("No inbounds found with ", subId)
|
||||
}
|
||||
|
||||
s.datepicker, err = s.settingService.GetDatepicker()
|
||||
@@ -73,7 +78,11 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
|
||||
if client.Enable && client.SubID == subId {
|
||||
link := s.getLink(inbound, client.Email)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,8 +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)
|
||||
return result, header, nil
|
||||
return result, lastOnline, traffic, nil
|
||||
}
|
||||
|
||||
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||
@@ -171,9 +179,15 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
if inbound.Protocol != model.VMESS {
|
||||
return ""
|
||||
}
|
||||
var address string
|
||||
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
||||
address = s.address
|
||||
} else {
|
||||
address = inbound.Listen
|
||||
}
|
||||
obj := map[string]any{
|
||||
"v": "2",
|
||||
"add": s.address,
|
||||
"add": address,
|
||||
"port": inbound.Port,
|
||||
"type": "none",
|
||||
}
|
||||
@@ -309,7 +323,13 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
|
||||
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
var address string
|
||||
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
||||
address = s.address
|
||||
} else {
|
||||
address = inbound.Listen
|
||||
}
|
||||
|
||||
if inbound.Protocol != model.VLESS {
|
||||
return ""
|
||||
}
|
||||
@@ -329,6 +349,13 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
params := make(map[string]string)
|
||||
params["type"] = streamNetwork
|
||||
|
||||
// Add encryption parameter for VLESS from inbound settings
|
||||
var settings map[string]any
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
if encryption, ok := settings["encryption"].(string); ok {
|
||||
params["encryption"] = encryption
|
||||
}
|
||||
|
||||
switch streamNetwork {
|
||||
case "tcp":
|
||||
tcp, _ := stream["tcpSettings"].(map[string]any)
|
||||
@@ -437,6 +464,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -503,7 +535,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
|
||||
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
var address string
|
||||
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
||||
address = s.address
|
||||
} else {
|
||||
address = inbound.Listen
|
||||
}
|
||||
if inbound.Protocol != model.Trojan {
|
||||
return ""
|
||||
}
|
||||
@@ -627,6 +664,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -694,7 +736,12 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
}
|
||||
|
||||
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
var address string
|
||||
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
||||
address = s.address
|
||||
} else {
|
||||
address = inbound.Listen
|
||||
}
|
||||
if inbound.Protocol != model.Shadowsocks {
|
||||
return ""
|
||||
}
|
||||
@@ -985,3 +1032,189 @@ func searchHost(headers any) string {
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// PageData is a view model for subpage.html
|
||||
// PageData contains data for rendering the subscription information page.
|
||||
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.
|
||||
// ResolveRequest extracts scheme, host, and header information from an HTTP request.
|
||||
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 subscription URLs for a given subscription ID.
|
||||
// It prioritizes configured URIs, then individual settings, and finally falls back to request-derived components.
|
||||
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) {
|
||||
// Input validation
|
||||
if subId == "" {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// Get configured URIs first (highest priority)
|
||||
configuredSubURI, _ := s.settingService.GetSubURI()
|
||||
configuredSubJsonURI, _ := s.settingService.GetSubJsonURI()
|
||||
|
||||
// Determine base scheme and host (cached to avoid duplicate calls)
|
||||
var baseScheme, baseHostWithPort string
|
||||
if configuredSubURI == "" || configuredSubJsonURI == "" {
|
||||
baseScheme, baseHostWithPort = s.getBaseSchemeAndHost(scheme, hostWithPort)
|
||||
}
|
||||
|
||||
// Build subscription URL
|
||||
subURL = s.buildSingleURL(configuredSubURI, baseScheme, baseHostWithPort, subPath, subId)
|
||||
|
||||
// Build JSON subscription URL
|
||||
subJsonURL = s.buildSingleURL(configuredSubJsonURI, baseScheme, baseHostWithPort, subJsonPath, subId)
|
||||
|
||||
return subURL, subJsonURL
|
||||
}
|
||||
|
||||
// getBaseSchemeAndHost determines the base scheme and host from settings or falls back to request values
|
||||
func (s *SubService) getBaseSchemeAndHost(requestScheme, requestHostWithPort string) (string, string) {
|
||||
subDomain, err := s.settingService.GetSubDomain()
|
||||
if err != nil || subDomain == "" {
|
||||
return requestScheme, requestHostWithPort
|
||||
}
|
||||
|
||||
// Get port and TLS settings
|
||||
subPort, _ := s.settingService.GetSubPort()
|
||||
subKeyFile, _ := s.settingService.GetSubKeyFile()
|
||||
subCertFile, _ := s.settingService.GetSubCertFile()
|
||||
|
||||
// Determine scheme from TLS configuration
|
||||
scheme := "http"
|
||||
if subKeyFile != "" && subCertFile != "" {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
// Build host:port, always include port for clarity
|
||||
hostWithPort := fmt.Sprintf("%s:%d", subDomain, subPort)
|
||||
|
||||
return scheme, hostWithPort
|
||||
}
|
||||
|
||||
// buildSingleURL constructs a single URL using configured URI or base components
|
||||
func (s *SubService) buildSingleURL(configuredURI, baseScheme, baseHostWithPort, basePath, subId string) string {
|
||||
if configuredURI != "" {
|
||||
return s.joinPathWithID(configuredURI, subId)
|
||||
}
|
||||
|
||||
baseURL := fmt.Sprintf("%s://%s", baseScheme, baseHostWithPort)
|
||||
return s.joinPathWithID(baseURL+basePath, subId)
|
||||
}
|
||||
|
||||
// joinPathWithID safely joins a base path with a subscription ID
|
||||
func (s *SubService) joinPathWithID(basePath, subId string) string {
|
||||
if strings.HasSuffix(basePath, "/") {
|
||||
return basePath + subId
|
||||
}
|
||||
return basePath + "/" + subId
|
||||
}
|
||||
|
||||
// BuildPageData parses header and prepares the template view model.
|
||||
// BuildPageData constructs page data for rendering the subscription information page.
|
||||
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string, basePath string) PageData {
|
||||
download := common.FormatTraffic(traffic.Down)
|
||||
upload := common.FormatTraffic(traffic.Up)
|
||||
total := "∞"
|
||||
used := common.FormatTraffic(traffic.Up + traffic.Down)
|
||||
remained := ""
|
||||
if traffic.Total > 0 {
|
||||
total = common.FormatTraffic(traffic.Total)
|
||||
left := max(traffic.Total-(traffic.Up+traffic.Down), 0)
|
||||
remained = common.FormatTraffic(left)
|
||||
}
|
||||
|
||||
datepicker := s.datepicker
|
||||
if datepicker == "" {
|
||||
datepicker = "gregorian"
|
||||
}
|
||||
|
||||
return PageData{
|
||||
Host: hostHeader,
|
||||
BasePath: basePath,
|
||||
SId: subId,
|
||||
Download: download,
|
||||
Upload: upload,
|
||||
Total: total,
|
||||
Used: used,
|
||||
Remained: remained,
|
||||
Expire: traffic.ExpiryTime / 1000,
|
||||
LastOnline: lastOnline,
|
||||
Datepicker: datepicker,
|
||||
DownloadByte: traffic.Down,
|
||||
UploadByte: traffic.Up,
|
||||
TotalByte: traffic.Total,
|
||||
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
|
||||
}
|
||||
|
||||
760
update.sh
Executable file
760
update.sh
Executable file
@@ -0,0 +1,760 @@
|
||||
#!/bin/bash
|
||||
|
||||
red='\033[0;31m'
|
||||
green='\033[0;32m'
|
||||
blue='\033[0;34m'
|
||||
yellow='\033[0;33m'
|
||||
plain='\033[0m'
|
||||
|
||||
xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}"
|
||||
xui_service="${XUI_SERVICE:=/etc/systemd/system}"
|
||||
|
||||
# Don't edit this config
|
||||
b_source="${BASH_SOURCE[0]}"
|
||||
while [ -h "$b_source" ]; do
|
||||
b_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)"
|
||||
b_source="$(readlink "$b_source")"
|
||||
[[ $b_source != /* ]] && b_source="$b_dir/$b_source"
|
||||
done
|
||||
cur_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)"
|
||||
script_name=$(basename "$0")
|
||||
|
||||
# Check command exist function
|
||||
_command_exists() {
|
||||
type "$1" &>/dev/null
|
||||
}
|
||||
|
||||
# Fail, log and exit script function
|
||||
_fail() {
|
||||
local msg=${1}
|
||||
echo -e "${red}${msg}${plain}"
|
||||
exit 2
|
||||
}
|
||||
|
||||
# check root
|
||||
[[ $EUID -ne 0 ]] && _fail "FATAL ERROR: Please run this script with root privilege."
|
||||
|
||||
if _command_exists curl; then
|
||||
curl_bin=$(which curl)
|
||||
else
|
||||
_fail "ERROR: Command 'curl' not found."
|
||||
fi
|
||||
|
||||
# Check OS and set release variable
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
source /etc/os-release
|
||||
release=$ID
|
||||
elif [[ -f /usr/lib/os-release ]]; then
|
||||
source /usr/lib/os-release
|
||||
release=$ID
|
||||
else
|
||||
_fail "Failed to check the system OS, please contact the author!"
|
||||
fi
|
||||
echo "The OS release is: $release"
|
||||
|
||||
arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64 | x64 | amd64) echo 'amd64' ;;
|
||||
i*86 | x86) echo '386' ;;
|
||||
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
|
||||
armv7* | armv7 | arm) echo 'armv7' ;;
|
||||
armv6* | armv6) echo 'armv6' ;;
|
||||
armv5* | armv5) echo 'armv5' ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" >/dev/null 2>&1 && exit 2;;
|
||||
esac
|
||||
}
|
||||
|
||||
echo "Arch: $(arch)"
|
||||
|
||||
# Simple helpers
|
||||
is_ipv4() {
|
||||
[[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1
|
||||
}
|
||||
is_ipv6() {
|
||||
[[ "$1" =~ : ]] && return 0 || return 1
|
||||
}
|
||||
is_ip() {
|
||||
is_ipv4 "$1" || is_ipv6 "$1"
|
||||
}
|
||||
is_domain() {
|
||||
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1
|
||||
}
|
||||
|
||||
gen_random_string() {
|
||||
local length="$1"
|
||||
local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' </dev/urandom | fold -w "$length" | head -n 1)
|
||||
echo "$random_string"
|
||||
}
|
||||
|
||||
install_base() {
|
||||
echo -e "${green}Updating and install dependency packages...${plain}"
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
apt-get update >/dev/null 2>&1 && apt-get install -y -q curl tar tzdata openssl socat >/dev/null 2>&1
|
||||
;;
|
||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
||||
dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata openssl socat >/dev/null 2>&1
|
||||
;;
|
||||
centos)
|
||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
||||
yum -y update >/dev/null 2>&1 && yum install -y -q curl tar tzdata openssl socat >/dev/null 2>&1
|
||||
else
|
||||
dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata openssl socat >/dev/null 2>&1
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm curl tar tzdata openssl socat >/dev/null 2>&1
|
||||
;;
|
||||
opensuse-tumbleweed | opensuse-leap)
|
||||
zypper refresh >/dev/null 2>&1 && zypper -q install -y curl tar timezone openssl socat >/dev/null 2>&1
|
||||
;;
|
||||
alpine)
|
||||
apk update >/dev/null 2>&1 && apk add curl tar tzdata openssl socat >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
apt-get update >/dev/null 2>&1 && apt install -y -q curl tar tzdata openssl socat >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_acme() {
|
||||
echo -e "${green}Installing acme.sh for SSL certificate management...${plain}"
|
||||
cd ~ || return 1
|
||||
curl -s https://get.acme.sh | sh >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Failed to install acme.sh${plain}"
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}acme.sh installed successfully${plain}"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
setup_ssl_certificate() {
|
||||
local domain="$1"
|
||||
local server_ip="$2"
|
||||
local existing_port="$3"
|
||||
local existing_webBasePath="$4"
|
||||
|
||||
echo -e "${green}Setting up SSL certificate...${plain}"
|
||||
|
||||
# Check if acme.sh is installed
|
||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||
install_acme
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create certificate directory
|
||||
local certPath="/root/cert/${domain}"
|
||||
mkdir -p "$certPath"
|
||||
|
||||
# Issue certificate
|
||||
echo -e "${green}Issuing SSL certificate for ${domain}...${plain}"
|
||||
echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
|
||||
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1
|
||||
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Failed to issue certificate for ${domain}${plain}"
|
||||
echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}"
|
||||
rm -rf ~/.acme.sh/${domain} 2>/dev/null
|
||||
rm -rf "$certPath" 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Install certificate
|
||||
~/.acme.sh/acme.sh --installcert -d ${domain} \
|
||||
--key-file /root/cert/${domain}/privkey.pem \
|
||||
--fullchain-file /root/cert/${domain}/fullchain.pem \
|
||||
--reloadcmd "systemctl restart x-ui" >/dev/null 2>&1
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Failed to install certificate${plain}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Enable auto-renew
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
|
||||
chmod 755 $certPath/* 2>/dev/null
|
||||
|
||||
# Set certificate for panel
|
||||
local webCertFile="/root/cert/${domain}/fullchain.pem"
|
||||
local webKeyFile="/root/cert/${domain}/privkey.pem"
|
||||
|
||||
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
|
||||
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1
|
||||
echo -e "${green}SSL certificate installed and configured successfully!${plain}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${yellow}Certificate files not found${plain}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Fallback: generate a self-signed certificate (not publicly trusted)
|
||||
setup_self_signed_certificate() {
|
||||
local name="$1" # domain or IP to place in SAN
|
||||
local certDir="/root/cert/selfsigned"
|
||||
|
||||
echo -e "${yellow}Generating a self-signed certificate (not publicly trusted)...${plain}"
|
||||
|
||||
mkdir -p "$certDir"
|
||||
|
||||
local sanExt=""
|
||||
if is_ip "$name"; then
|
||||
sanExt="IP:${name}"
|
||||
else
|
||||
sanExt="DNS:${name}"
|
||||
fi
|
||||
|
||||
# Try -addext; fallback to config if not supported
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||
-keyout "${certDir}/privkey.pem" \
|
||||
-out "${certDir}/fullchain.pem" \
|
||||
-subj "/CN=${name}" \
|
||||
-addext "subjectAltName=${sanExt}" >/dev/null 2>&1
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
local tmpCfg="${certDir}/openssl.cnf"
|
||||
cat > "$tmpCfg" <<EOF
|
||||
[req]
|
||||
distinguished_name=req_distinguished_name
|
||||
req_extensions=v3_req
|
||||
[req_distinguished_name]
|
||||
[v3_req]
|
||||
subjectAltName=${sanExt}
|
||||
EOF
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||
-keyout "${certDir}/privkey.pem" \
|
||||
-out "${certDir}/fullchain.pem" \
|
||||
-subj "/CN=${name}" \
|
||||
-config "$tmpCfg" -extensions v3_req >/dev/null 2>&1
|
||||
rm -f "$tmpCfg"
|
||||
fi
|
||||
|
||||
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
|
||||
echo -e "${red}Failed to generate self-signed certificate${plain}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
chmod 755 ${certDir}/* 2>/dev/null
|
||||
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1
|
||||
echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}"
|
||||
return 0
|
||||
}
|
||||
# Comprehensive manual SSL certificate issuance via acme.sh
|
||||
ssl_cert_issue() {
|
||||
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')
|
||||
local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
|
||||
|
||||
# check for acme.sh first
|
||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||
echo "acme.sh could not be found. Installing now..."
|
||||
cd ~ || return 1
|
||||
curl -s https://get.acme.sh | sh
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Failed to install acme.sh${plain}"
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}acme.sh installed successfully${plain}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# get the domain here, and we need to verify it
|
||||
local domain=""
|
||||
while true; do
|
||||
read -rp "Please enter your domain name: " domain
|
||||
domain="${domain// /}" # Trim whitespace
|
||||
|
||||
if [[ -z "$domain" ]]; then
|
||||
echo -e "${red}Domain name cannot be empty. Please try again.${plain}"
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! is_domain "$domain"; then
|
||||
echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}"
|
||||
continue
|
||||
fi
|
||||
|
||||
break
|
||||
done
|
||||
echo -e "${green}Your domain is: ${domain}, checking it...${plain}"
|
||||
|
||||
# check if there already exists a certificate
|
||||
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
|
||||
if [ "${currentCert}" == "${domain}" ]; then
|
||||
local certInfo=$(~/.acme.sh/acme.sh --list)
|
||||
echo -e "${red}System already has certificates for this domain. Cannot issue again.${plain}"
|
||||
echo -e "${yellow}Current certificate details:${plain}"
|
||||
echo "$certInfo"
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}Your domain is ready for issuing certificates now...${plain}"
|
||||
fi
|
||||
|
||||
# create a directory for the certificate
|
||||
certPath="/root/cert/${domain}"
|
||||
if [ ! -d "$certPath" ]; then
|
||||
mkdir -p "$certPath"
|
||||
else
|
||||
rm -rf "$certPath"
|
||||
mkdir -p "$certPath"
|
||||
fi
|
||||
|
||||
# get the port number for the standalone server
|
||||
local WebPort=80
|
||||
read -rp "Please choose which port to use (default is 80): " WebPort
|
||||
if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then
|
||||
echo -e "${yellow}Your input ${WebPort} is invalid, will use default port 80.${plain}"
|
||||
WebPort=80
|
||||
fi
|
||||
echo -e "${green}Will use port: ${WebPort} to issue certificates. Please make sure this port is open.${plain}"
|
||||
|
||||
# Stop panel temporarily
|
||||
echo -e "${yellow}Stopping panel temporarily...${plain}"
|
||||
systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null
|
||||
|
||||
# issue the certificate
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
||||
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Issuing certificate failed, please check logs.${plain}"
|
||||
rm -rf ~/.acme.sh/${domain}
|
||||
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}"
|
||||
fi
|
||||
|
||||
# Setup reload command
|
||||
reloadCmd="systemctl restart x-ui || rc-service x-ui restart"
|
||||
echo -e "${green}Default --reloadcmd for ACME is: ${yellow}systemctl restart x-ui || rc-service x-ui restart${plain}"
|
||||
echo -e "${green}This command will run on every certificate issue and renew.${plain}"
|
||||
read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd
|
||||
if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then
|
||||
echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; systemctl restart x-ui"
|
||||
echo -e "${green}\t2.${plain} Input your own command"
|
||||
echo -e "${green}\t0.${plain} Keep default reloadcmd"
|
||||
read -rp "Choose an option: " choice
|
||||
case "$choice" in
|
||||
1)
|
||||
echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}"
|
||||
reloadCmd="systemctl reload nginx ; systemctl restart x-ui"
|
||||
;;
|
||||
2)
|
||||
echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}"
|
||||
read -rp "Please enter your custom reloadcmd: " reloadCmd
|
||||
echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}"
|
||||
;;
|
||||
*)
|
||||
echo -e "${green}Keeping default reloadcmd${plain}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# install the certificate
|
||||
~/.acme.sh/acme.sh --installcert -d ${domain} \
|
||||
--key-file /root/cert/${domain}/privkey.pem \
|
||||
--fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${red}Installing certificate failed, exiting.${plain}"
|
||||
rm -rf ~/.acme.sh/${domain}
|
||||
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
|
||||
return 1
|
||||
else
|
||||
echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}"
|
||||
fi
|
||||
|
||||
# enable auto-renew
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}"
|
||||
ls -lah /root/cert/${domain}/
|
||||
chmod 755 $certPath/*
|
||||
else
|
||||
echo -e "${green}Auto renew succeeded, certificate details:${plain}"
|
||||
ls -lah /root/cert/${domain}/
|
||||
chmod 755 $certPath/*
|
||||
fi
|
||||
|
||||
# Restart panel
|
||||
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
|
||||
|
||||
# Prompt user to set panel paths after successful certificate installation
|
||||
read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel
|
||||
if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then
|
||||
local webCertFile="/root/cert/${domain}/fullchain.pem"
|
||||
local webKeyFile="/root/cert/${domain}/privkey.pem"
|
||||
|
||||
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
|
||||
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
|
||||
echo -e "${green}Certificate paths set for the panel${plain}"
|
||||
echo -e "${green}Certificate File: $webCertFile${plain}"
|
||||
echo -e "${green}Private Key File: $webKeyFile${plain}"
|
||||
echo ""
|
||||
echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}"
|
||||
echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}"
|
||||
systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null
|
||||
else
|
||||
echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}"
|
||||
fi
|
||||
else
|
||||
echo -e "${yellow}Skipping panel path setting.${plain}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
# Unified interactive SSL setup (domain or self-signed)
|
||||
# Sets global `SSL_HOST` to the chosen domain/IP
|
||||
prompt_and_setup_ssl() {
|
||||
local panel_port="$1"
|
||||
local web_base_path="$2" # expected without leading slash
|
||||
local server_ip="$3"
|
||||
|
||||
local ssl_choice=""
|
||||
|
||||
echo -e "${yellow}Choose SSL certificate setup method:${plain}"
|
||||
echo -e "${green}1.${plain} Let's Encrypt (domain required, recommended)"
|
||||
echo -e "${green}2.${plain} Self-signed certificate (for testing/local use)"
|
||||
read -rp "Choose an option (default 2): " ssl_choice
|
||||
ssl_choice="${ssl_choice// /}" # Trim whitespace
|
||||
|
||||
# Default to 2 (self-signed) if not 1
|
||||
if [[ "$ssl_choice" != "1" ]]; then
|
||||
ssl_choice="2"
|
||||
fi
|
||||
|
||||
case "$ssl_choice" in
|
||||
1)
|
||||
# User chose Let's Encrypt domain option
|
||||
echo -e "${green}Using ssl_cert_issue() for comprehensive domain setup...${plain}"
|
||||
ssl_cert_issue
|
||||
# Extract the domain that was used from the certificate
|
||||
local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}')
|
||||
if [[ -n "${cert_domain}" ]]; then
|
||||
SSL_HOST="${cert_domain}"
|
||||
echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}"
|
||||
else
|
||||
echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}"
|
||||
SSL_HOST="${server_ip}"
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
# User chose self-signed option
|
||||
# Stop panel if running
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui stop >/dev/null 2>&1
|
||||
else
|
||||
systemctl stop x-ui >/dev/null 2>&1
|
||||
fi
|
||||
echo -e "${yellow}Using server IP for self-signed certificate: ${server_ip}${plain}"
|
||||
setup_self_signed_certificate "${server_ip}"
|
||||
if [ $? -eq 0 ]; then
|
||||
SSL_HOST="${server_ip}"
|
||||
echo -e "${green}✓ Self-signed SSL configured successfully${plain}"
|
||||
else
|
||||
echo -e "${red}✗ Self-signed SSL setup failed${plain}"
|
||||
SSL_HOST="${server_ip}"
|
||||
fi
|
||||
# Start panel after SSL is configured
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui start >/dev/null 2>&1
|
||||
else
|
||||
systemctl start x-ui >/dev/null 2>&1
|
||||
fi
|
||||
;;
|
||||
0)
|
||||
echo -e "${yellow}Skipping SSL setup${plain}"
|
||||
SSL_HOST="${server_ip}"
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
|
||||
SSL_HOST="${server_ip}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
config_after_update() {
|
||||
echo -e "${yellow}x-ui settings:${plain}"
|
||||
${xui_folder}/x-ui setting -show true
|
||||
${xui_folder}/x-ui migrate
|
||||
|
||||
# Properly detect empty cert by checking if cert: line exists and has content after it
|
||||
local existing_cert=$(${xui_folder}/x-ui setting -getCert true 2>/dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
|
||||
local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
||||
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##')
|
||||
|
||||
# Get server IP
|
||||
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_bin} -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
||||
if [[ -n "${server_ip}" ]]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Handle missing/short webBasePath
|
||||
if [[ ${#existing_webBasePath} -lt 4 ]]; then
|
||||
echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}"
|
||||
local config_webBasePath=$(gen_random_string 18)
|
||||
${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}"
|
||||
existing_webBasePath="${config_webBasePath}"
|
||||
echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
|
||||
fi
|
||||
|
||||
# Check and prompt for SSL if missing
|
||||
if [[ -z "$existing_cert" ]]; then
|
||||
echo ""
|
||||
echo -e "${red}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${red} ⚠ NO SSL CERTIFICATE DETECTED ⚠ ${plain}"
|
||||
echo -e "${red}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}"
|
||||
echo -e "${yellow}Let's Encrypt requires a domain name; IP certs are not issued. Use self-signed for IP.${plain}"
|
||||
echo ""
|
||||
|
||||
if [[ -z "${server_ip}" ]]; then
|
||||
echo -e "${red}Failed to detect server IP${plain}"
|
||||
echo -e "${yellow}Please configure SSL manually using: x-ui${plain}"
|
||||
return
|
||||
fi
|
||||
|
||||
# Prompt and setup SSL (domain or self-signed)
|
||||
prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}"
|
||||
|
||||
echo ""
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green} Panel Access Information ${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${yellow}⚠ SSL Certificate: Enabled and configured${plain}"
|
||||
else
|
||||
echo -e "${green}SSL certificate is already configured${plain}"
|
||||
# Show access URL with existing certificate
|
||||
local cert_domain=$(basename "$(dirname "$existing_cert")")
|
||||
echo ""
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green} Panel Access Information ${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
echo -e "${green}Access URL: https://${cert_domain}:${existing_port}/${existing_webBasePath}${plain}"
|
||||
echo -e "${green}═══════════════════════════════════════════${plain}"
|
||||
fi
|
||||
}
|
||||
|
||||
update_x-ui() {
|
||||
cd ${xui_folder%/x-ui}/
|
||||
|
||||
if [ -f "${xui_folder}/x-ui" ]; then
|
||||
current_xui_version=$(${xui_folder}/x-ui -v)
|
||||
echo -e "${green}Current x-ui version: ${current_xui_version}${plain}"
|
||||
else
|
||||
_fail "ERROR: Current x-ui version: unknown"
|
||||
fi
|
||||
|
||||
echo -e "${green}Downloading new x-ui version...${plain}"
|
||||
|
||||
tag_version=$(${curl_bin} -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" 2>/dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$tag_version" ]]; then
|
||||
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
|
||||
tag_version=$(${curl_bin} -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$tag_version" ]]; then
|
||||
_fail "ERROR: Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later"
|
||||
fi
|
||||
fi
|
||||
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
|
||||
${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
|
||||
${curl_bin} -4fLRo ${xui_folder}-linux-$(arch).tar.gz -z ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e ${xui_folder}/ ]]; then
|
||||
echo -e "${green}Stopping x-ui...${plain}"
|
||||
if [[ $release == "alpine" ]]; then
|
||||
if [ -f "/etc/init.d/x-ui" ]; then
|
||||
rc-service x-ui stop >/dev/null 2>&1
|
||||
rc-update del x-ui >/dev/null 2>&1
|
||||
echo -e "${green}Removing old service unit version...${plain}"
|
||||
rm -f /etc/init.d/x-ui >/dev/null 2>&1
|
||||
else
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
_fail "ERROR: x-ui service unit not installed."
|
||||
fi
|
||||
else
|
||||
if [ -f "${xui_service}/x-ui.service" ]; then
|
||||
systemctl stop x-ui >/dev/null 2>&1
|
||||
systemctl disable x-ui >/dev/null 2>&1
|
||||
echo -e "${green}Removing old systemd unit version...${plain}"
|
||||
rm ${xui_service}/x-ui.service -f >/dev/null 2>&1
|
||||
systemctl daemon-reload >/dev/null 2>&1
|
||||
else
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
_fail "ERROR: x-ui systemd unit not installed."
|
||||
fi
|
||||
fi
|
||||
echo -e "${green}Removing old x-ui version...${plain}"
|
||||
rm ${xui_folder} -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui.service -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui.service.debian -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui.service.rhel -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/x-ui.sh -f >/dev/null 2>&1
|
||||
echo -e "${green}Removing old xray version...${plain}"
|
||||
rm ${xui_folder}/bin/xray-linux-amd64 -f >/dev/null 2>&1
|
||||
echo -e "${green}Removing old README and LICENSE file...${plain}"
|
||||
rm ${xui_folder}/bin/README.md -f >/dev/null 2>&1
|
||||
rm ${xui_folder}/bin/LICENSE -f >/dev/null 2>&1
|
||||
else
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
_fail "ERROR: x-ui not installed."
|
||||
fi
|
||||
|
||||
echo -e "${green}Installing new x-ui version...${plain}"
|
||||
tar zxvf x-ui-linux-$(arch).tar.gz >/dev/null 2>&1
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
cd x-ui >/dev/null 2>&1
|
||||
chmod +x x-ui >/dev/null 2>&1
|
||||
|
||||
# Check the system's architecture and rename the file accordingly
|
||||
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
|
||||
mv bin/xray-linux-$(arch) bin/xray-linux-arm >/dev/null 2>&1
|
||||
chmod +x bin/xray-linux-arm >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
chmod +x x-ui bin/xray-linux-$(arch) >/dev/null 2>&1
|
||||
|
||||
echo -e "${green}Downloading and installing x-ui.sh script...${plain}"
|
||||
${curl_bin} -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${yellow}Trying to fetch x-ui with IPv4...${plain}"
|
||||
${curl_bin} -4fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub"
|
||||
fi
|
||||
fi
|
||||
|
||||
chmod +x ${xui_folder}/x-ui.sh >/dev/null 2>&1
|
||||
chmod +x /usr/bin/x-ui >/dev/null 2>&1
|
||||
mkdir -p /var/log/x-ui >/dev/null 2>&1
|
||||
|
||||
echo -e "${green}Changing owner...${plain}"
|
||||
chown -R root:root ${xui_folder} >/dev/null 2>&1
|
||||
|
||||
if [ -f "${xui_folder}/bin/config.json" ]; then
|
||||
echo -e "${green}Changing on config file permissions...${plain}"
|
||||
chmod 640 ${xui_folder}/bin/config.json >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
if [[ $release == "alpine" ]]; then
|
||||
echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}"
|
||||
${curl_bin} -fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
${curl_bin} -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub"
|
||||
fi
|
||||
fi
|
||||
chmod +x /etc/init.d/x-ui >/dev/null 2>&1
|
||||
chown root:root /etc/init.d/x-ui >/dev/null 2>&1
|
||||
rc-update add x-ui >/dev/null 2>&1
|
||||
rc-service x-ui start >/dev/null 2>&1
|
||||
else
|
||||
if [ -f "x-ui.service" ]; then
|
||||
echo -e "${green}Installing systemd unit...${plain}"
|
||||
cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Failed to copy x-ui.service${plain}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
service_installed=false
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
if [ -f "x-ui.service.debian" ]; then
|
||||
echo -e "${green}Installing debian-like systemd unit...${plain}"
|
||||
cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
if [[ $? -eq 0 ]]; then
|
||||
service_installed=true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [ -f "x-ui.service.rhel" ]; then
|
||||
echo -e "${green}Installing rhel-like systemd unit...${plain}"
|
||||
cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
if [[ $? -eq 0 ]]; then
|
||||
service_installed=true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# If service file not found in tar.gz, download from GitHub
|
||||
if [ "$service_installed" = false ]; then
|
||||
echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}"
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Failed to install x-ui.service from GitHub${plain}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
chmod 644 ${xui_service}/x-ui.service >/dev/null 2>&1
|
||||
systemctl daemon-reload >/dev/null 2>&1
|
||||
systemctl enable x-ui >/dev/null 2>&1
|
||||
systemctl start x-ui >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
config_after_update
|
||||
|
||||
echo -e "${green}x-ui ${tag_version}${plain} updating finished, it is running now..."
|
||||
echo -e ""
|
||||
echo -e "┌───────────────────────────────────────────────────────┐
|
||||
│ ${blue}x-ui control menu usages (subcommands):${plain} │
|
||||
│ │
|
||||
│ ${blue}x-ui${plain} - Admin Management Script │
|
||||
│ ${blue}x-ui start${plain} - Start │
|
||||
│ ${blue}x-ui stop${plain} - Stop │
|
||||
│ ${blue}x-ui restart${plain} - Restart │
|
||||
│ ${blue}x-ui status${plain} - Current Status │
|
||||
│ ${blue}x-ui settings${plain} - Current Settings │
|
||||
│ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │
|
||||
│ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup │
|
||||
│ ${blue}x-ui log${plain} - Check logs │
|
||||
│ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
|
||||
│ ${blue}x-ui update${plain} - Update │
|
||||
│ ${blue}x-ui legacy${plain} - Legacy version │
|
||||
│ ${blue}x-ui install${plain} - Install │
|
||||
│ ${blue}x-ui uninstall${plain} - Uninstall │
|
||||
└───────────────────────────────────────────────────────┘"
|
||||
}
|
||||
|
||||
echo -e "${green}Running...${plain}"
|
||||
install_base
|
||||
update_x-ui $1
|
||||
@@ -1,22 +1,26 @@
|
||||
// Package common provides common utility functions for error handling, formatting, and multi-error management.
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"x-ui/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
)
|
||||
|
||||
// NewErrorf creates a new error with formatted message.
|
||||
func NewErrorf(format string, a ...any) error {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// NewError creates a new error from the given arguments.
|
||||
func NewError(a ...any) error {
|
||||
msg := fmt.Sprintln(a...)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// Recover handles panic recovery and logs the panic error if a message is provided.
|
||||
func Recover(msg string) any {
|
||||
panicErr := recover()
|
||||
if panicErr != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// FormatTraffic formats traffic bytes into human-readable units (B, KB, MB, GB, TB, PB).
|
||||
func FormatTraffic(trafficBytes int64) string {
|
||||
units := []string{"B", "KB", "MB", "GB", "TB", "PB"}
|
||||
unitIndex := 0
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// multiError represents a collection of errors.
|
||||
type multiError []error
|
||||
|
||||
// Error returns a string representation of all errors joined with " | ".
|
||||
func (e multiError) Error() string {
|
||||
var r strings.Builder
|
||||
r.WriteString("multierr: ")
|
||||
@@ -16,6 +18,7 @@ func (e multiError) Error() string {
|
||||
return r.String()
|
||||
}
|
||||
|
||||
// Combine combines multiple errors into a single error, filtering out nil errors.
|
||||
func Combine(maybeError ...error) error {
|
||||
var errs multiError
|
||||
for _, err := range maybeError {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
// Package crypto provides cryptographic utilities for password hashing and verification.
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// HashPasswordAsBcrypt generates a bcrypt hash of the given password.
|
||||
func HashPasswordAsBcrypt(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(hash), err
|
||||
}
|
||||
|
||||
// CheckPasswordHash verifies if the given password matches the bcrypt hash.
|
||||
func CheckPasswordHash(hash, password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// Package json_util provides JSON utilities including a custom RawMessage type.
|
||||
package json_util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// RawMessage is a custom JSON raw message type that marshals empty slices as "null".
|
||||
type RawMessage []byte
|
||||
|
||||
// MarshalJSON: Customize json.RawMessage default behavior
|
||||
// MarshalJSON customizes the JSON marshaling behavior for RawMessage.
|
||||
// Empty RawMessage values are marshaled as "null" instead of "[]".
|
||||
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||
if len(m) == 0 {
|
||||
return []byte("null"), nil
|
||||
@@ -14,7 +17,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON: sets *m to a copy of data.
|
||||
// UnmarshalJSON sets *m to a copy of the JSON data.
|
||||
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
||||
if m == nil {
|
||||
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
|
||||
|
||||
142
util/ldap/ldap.go
Normal file
142
util/ldap/ldap.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package ldaputil
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
UseTLS bool
|
||||
BindDN string
|
||||
Password string
|
||||
BaseDN string
|
||||
UserFilter string
|
||||
UserAttr string
|
||||
FlagField string
|
||||
TruthyVals []string
|
||||
Invert bool
|
||||
}
|
||||
|
||||
// FetchVlessFlags returns map[email]enabled
|
||||
func FetchVlessFlags(cfg Config) (map[string]bool, error) {
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
var conn *ldap.Conn
|
||||
var err error
|
||||
if cfg.UseTLS {
|
||||
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
|
||||
} else {
|
||||
conn, err = ldap.Dial("tcp", addr)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if cfg.BindDN != "" {
|
||||
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.UserFilter == "" {
|
||||
cfg.UserFilter = "(objectClass=person)"
|
||||
}
|
||||
if cfg.UserAttr == "" {
|
||||
cfg.UserAttr = "mail"
|
||||
}
|
||||
// if field not set we fallback to legacy vless_enabled
|
||||
if cfg.FlagField == "" {
|
||||
cfg.FlagField = "vless_enabled"
|
||||
}
|
||||
|
||||
req := ldap.NewSearchRequest(
|
||||
cfg.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
cfg.UserFilter,
|
||||
[]string{cfg.UserAttr, cfg.FlagField},
|
||||
nil,
|
||||
)
|
||||
|
||||
res, err := conn.Search(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]bool, len(res.Entries))
|
||||
for _, e := range res.Entries {
|
||||
user := e.GetAttributeValue(cfg.UserAttr)
|
||||
if user == "" {
|
||||
continue
|
||||
}
|
||||
val := e.GetAttributeValue(cfg.FlagField)
|
||||
enabled := false
|
||||
for _, t := range cfg.TruthyVals {
|
||||
if val == t {
|
||||
enabled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if cfg.Invert {
|
||||
enabled = !enabled
|
||||
}
|
||||
result[user] = enabled
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password.
|
||||
func AuthenticateUser(cfg Config, username, password string) (bool, error) {
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
var conn *ldap.Conn
|
||||
var err error
|
||||
if cfg.UseTLS {
|
||||
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
|
||||
} else {
|
||||
conn, err = ldap.Dial("tcp", addr)
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Optional initial bind for search
|
||||
if cfg.BindDN != "" {
|
||||
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.UserFilter == "" {
|
||||
cfg.UserFilter = "(objectClass=person)"
|
||||
}
|
||||
if cfg.UserAttr == "" {
|
||||
cfg.UserAttr = "uid"
|
||||
}
|
||||
|
||||
// Build filter to find specific user
|
||||
filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UserAttr, ldap.EscapeFilter(username))
|
||||
req := ldap.NewSearchRequest(
|
||||
cfg.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false,
|
||||
filter,
|
||||
[]string{"dn"},
|
||||
nil,
|
||||
)
|
||||
res, err := conn.Search(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(res.Entries) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
userDN := res.Entries[0].DN
|
||||
// Try to bind as the user
|
||||
if err := conn.Bind(userDN, password); err != nil {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
// Package random provides utilities for generating random strings and numbers.
|
||||
package random
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -13,6 +15,8 @@ var (
|
||||
allSeq [62]rune
|
||||
)
|
||||
|
||||
// init initializes the character sequences used for random string generation.
|
||||
// It sets up arrays for numbers, lowercase letters, uppercase letters, and combinations.
|
||||
func init() {
|
||||
for i := 0; i < 10; i++ {
|
||||
numSeq[i] = rune('0' + i)
|
||||
@@ -33,14 +37,25 @@ func init() {
|
||||
copy(allSeq[len(numSeq)+len(lowerSeq):], upperSeq[:])
|
||||
}
|
||||
|
||||
// Seq generates a random string of length n containing alphanumeric characters (numbers, lowercase and uppercase letters).
|
||||
func Seq(n int) string {
|
||||
runes := make([]rune, n)
|
||||
for i := 0; i < n; i++ {
|
||||
runes[i] = allSeq[rand.Intn(len(allSeq))]
|
||||
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(allSeq))))
|
||||
if err != nil {
|
||||
panic("crypto/rand failed: " + err.Error())
|
||||
}
|
||||
runes[i] = allSeq[idx.Int64()]
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
// Num generates a random integer between 0 and n-1.
|
||||
func Num(n int) int {
|
||||
return rand.Intn(n)
|
||||
bn := big.NewInt(int64(n))
|
||||
r, err := rand.Int(rand.Reader, bn)
|
||||
if err != nil {
|
||||
panic("crypto/rand failed: " + err.Error())
|
||||
}
|
||||
return int(r.Int64())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Package reflect_util provides reflection utilities for working with struct fields and values.
|
||||
package reflect_util
|
||||
|
||||
import "reflect"
|
||||
|
||||
// GetFields returns all struct fields of the given reflect.Type.
|
||||
func GetFields(t reflect.Type) []reflect.StructField {
|
||||
num := t.NumField()
|
||||
fields := make([]reflect.StructField, 0, num)
|
||||
@@ -11,6 +13,7 @@ func GetFields(t reflect.Type) []reflect.StructField {
|
||||
return fields
|
||||
}
|
||||
|
||||
// GetFieldValues returns all field values of the given reflect.Value.
|
||||
func GetFieldValues(v reflect.Value) []reflect.Value {
|
||||
num := v.NumField()
|
||||
fields := make([]reflect.Value, 0, num)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Package sys provides system utilities for monitoring network connections and CPU usage.
|
||||
// Platform-specific implementations are provided for Windows, Linux, and macOS.
|
||||
package sys
|
||||
|
||||
import (
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func GetTCPCount() (int, error) {
|
||||
@@ -22,3 +27,69 @@ func GetUDPCount() (int, error) {
|
||||
}
|
||||
return len(stats), nil
|
||||
}
|
||||
|
||||
// --- CPU Utilization (macOS native) ---
|
||||
|
||||
// sysctl kern.cp_time returns an array of 5 longs: user, nice, sys, idle, intr.
|
||||
// We compute utilization deltas without cgo.
|
||||
var (
|
||||
cpuMu sync.Mutex
|
||||
lastTotals [5]uint64
|
||||
hasLastCPUT bool
|
||||
)
|
||||
|
||||
func CPUPercentRaw() (float64, error) {
|
||||
raw, err := unix.SysctlRaw("kern.cp_time")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Expect either 5*8 bytes (uint64) or 5*4 bytes (uint32)
|
||||
var out [5]uint64
|
||||
switch len(raw) {
|
||||
case 5 * 8:
|
||||
for i := 0; i < 5; i++ {
|
||||
out[i] = binary.LittleEndian.Uint64(raw[i*8 : (i+1)*8])
|
||||
}
|
||||
case 5 * 4:
|
||||
for i := 0; i < 5; i++ {
|
||||
out[i] = uint64(binary.LittleEndian.Uint32(raw[i*4 : (i+1)*4]))
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("unexpected kern.cp_time size: %d", len(raw))
|
||||
}
|
||||
|
||||
// user, nice, sys, idle, intr
|
||||
user := out[0]
|
||||
nice := out[1]
|
||||
sysv := out[2]
|
||||
idle := out[3]
|
||||
intr := out[4]
|
||||
|
||||
cpuMu.Lock()
|
||||
defer cpuMu.Unlock()
|
||||
|
||||
if !hasLastCPUT {
|
||||
lastTotals = out
|
||||
hasLastCPUT = true
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
dUser := user - lastTotals[0]
|
||||
dNice := nice - lastTotals[1]
|
||||
dSys := sysv - lastTotals[2]
|
||||
dIdle := idle - lastTotals[3]
|
||||
dIntr := intr - lastTotals[4]
|
||||
|
||||
lastTotals = out
|
||||
|
||||
totald := dUser + dNice + dSys + dIdle + dIntr
|
||||
if totald == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
busy := totald - dIdle
|
||||
pct := float64(busy) / float64(totald) * 100.0
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
return pct, nil
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func getLinesNum(filename string) (int, error) {
|
||||
@@ -41,6 +45,8 @@ func getLinesNum(filename string) (int, error) {
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
// GetTCPCount returns the number of active TCP connections by reading
|
||||
// /proc/net/tcp and /proc/net/tcp6 when available.
|
||||
func GetTCPCount() (int, error) {
|
||||
root := HostProc()
|
||||
|
||||
@@ -71,6 +77,8 @@ func GetUDPCount() (int, error) {
|
||||
return udp4 + udp6, nil
|
||||
}
|
||||
|
||||
// safeGetLinesNum returns 0 if the file does not exist, otherwise forwards
|
||||
// to getLinesNum to count the number of lines.
|
||||
func safeGetLinesNum(path string) (int, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
@@ -79,3 +87,99 @@ func safeGetLinesNum(path string) (int, error) {
|
||||
}
|
||||
return getLinesNum(path)
|
||||
}
|
||||
|
||||
// --- CPU Utilization (Linux native) ---
|
||||
|
||||
var (
|
||||
cpuMu sync.Mutex
|
||||
lastTotal uint64
|
||||
lastIdleAll uint64
|
||||
hasLast bool
|
||||
)
|
||||
|
||||
// CPUPercentRaw returns instantaneous total CPU utilization by reading /proc/stat.
|
||||
// First call initializes and returns 0; subsequent calls return busy/total * 100.
|
||||
func CPUPercentRaw() (float64, error) {
|
||||
f, err := os.Open("/proc/stat")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
rd := bufio.NewReader(f)
|
||||
line, err := rd.ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return 0, err
|
||||
}
|
||||
// Expect line like: cpu user nice system idle iowait irq softirq steal guest guest_nice
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 5 || fields[0] != "cpu" {
|
||||
return 0, fmt.Errorf("unexpected /proc/stat format")
|
||||
}
|
||||
|
||||
var nums []uint64
|
||||
for i := 1; i < len(fields); i++ {
|
||||
v, err := strconv.ParseUint(fields[i], 10, 64)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
nums = append(nums, v)
|
||||
}
|
||||
if len(nums) < 4 { // need at least user,nice,system,idle
|
||||
return 0, fmt.Errorf("insufficient cpu fields")
|
||||
}
|
||||
|
||||
// Conform with standard Linux CPU accounting
|
||||
var user, nice, system, idle, iowait, irq, softirq, steal uint64
|
||||
user = nums[0]
|
||||
if len(nums) > 1 {
|
||||
nice = nums[1]
|
||||
}
|
||||
if len(nums) > 2 {
|
||||
system = nums[2]
|
||||
}
|
||||
if len(nums) > 3 {
|
||||
idle = nums[3]
|
||||
}
|
||||
if len(nums) > 4 {
|
||||
iowait = nums[4]
|
||||
}
|
||||
if len(nums) > 5 {
|
||||
irq = nums[5]
|
||||
}
|
||||
if len(nums) > 6 {
|
||||
softirq = nums[6]
|
||||
}
|
||||
if len(nums) > 7 {
|
||||
steal = nums[7]
|
||||
}
|
||||
|
||||
idleAll := idle + iowait
|
||||
nonIdle := user + nice + system + irq + softirq + steal
|
||||
total := idleAll + nonIdle
|
||||
|
||||
cpuMu.Lock()
|
||||
defer cpuMu.Unlock()
|
||||
|
||||
if !hasLast {
|
||||
lastTotal = total
|
||||
lastIdleAll = idleAll
|
||||
hasLast = true
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
totald := total - lastTotal
|
||||
idled := idleAll - lastIdleAll
|
||||
lastTotal = total
|
||||
lastIdleAll = idleAll
|
||||
|
||||
if totald == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
busy := totald - idled
|
||||
pct := float64(busy) / float64(totald) * 100.0
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
return pct, nil
|
||||
}
|
||||
|
||||
@@ -5,10 +5,14 @@ package sys
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
// GetConnectionCount returns the number of active connections for the specified protocol ("tcp" or "udp").
|
||||
func GetConnectionCount(proto string) (int, error) {
|
||||
if proto != "tcp" && proto != "udp" {
|
||||
return 0, errors.New("invalid protocol")
|
||||
@@ -21,10 +25,92 @@ func GetConnectionCount(proto string) (int, error) {
|
||||
return len(stats), nil
|
||||
}
|
||||
|
||||
// GetTCPCount returns the number of active TCP connections.
|
||||
func GetTCPCount() (int, error) {
|
||||
return GetConnectionCount("tcp")
|
||||
}
|
||||
|
||||
// GetUDPCount returns the number of active UDP connections.
|
||||
func GetUDPCount() (int, error) {
|
||||
return GetConnectionCount("udp")
|
||||
}
|
||||
|
||||
// --- CPU Utilization (Windows native) ---
|
||||
|
||||
var (
|
||||
modKernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procGetSystemTimes = modKernel32.NewProc("GetSystemTimes")
|
||||
|
||||
cpuMu sync.Mutex
|
||||
lastIdle uint64
|
||||
lastKernel uint64
|
||||
lastUser uint64
|
||||
hasLast bool
|
||||
)
|
||||
|
||||
type filetime struct {
|
||||
LowDateTime uint32
|
||||
HighDateTime uint32
|
||||
}
|
||||
|
||||
// ftToUint64 converts a Windows FILETIME-like struct to a uint64 for
|
||||
// arithmetic and delta calculations used by CPUPercentRaw.
|
||||
func ftToUint64(ft filetime) uint64 {
|
||||
return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime)
|
||||
}
|
||||
|
||||
// CPUPercentRaw returns the instantaneous total CPU utilization percentage using
|
||||
// Windows GetSystemTimes across all logical processors. The first call returns 0
|
||||
// as it initializes the baseline. Subsequent calls compute deltas.
|
||||
func CPUPercentRaw() (float64, error) {
|
||||
var idleFT, kernelFT, userFT filetime
|
||||
r1, _, e1 := procGetSystemTimes.Call(
|
||||
uintptr(unsafe.Pointer(&idleFT)),
|
||||
uintptr(unsafe.Pointer(&kernelFT)),
|
||||
uintptr(unsafe.Pointer(&userFT)),
|
||||
)
|
||||
if r1 == 0 { // failure
|
||||
if e1 != nil {
|
||||
return 0, e1
|
||||
}
|
||||
return 0, syscall.GetLastError()
|
||||
}
|
||||
|
||||
idle := ftToUint64(idleFT)
|
||||
kernel := ftToUint64(kernelFT)
|
||||
user := ftToUint64(userFT)
|
||||
|
||||
cpuMu.Lock()
|
||||
defer cpuMu.Unlock()
|
||||
|
||||
if !hasLast {
|
||||
lastIdle = idle
|
||||
lastKernel = kernel
|
||||
lastUser = user
|
||||
hasLast = true
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
idleDelta := idle - lastIdle
|
||||
kernelDelta := kernel - lastKernel
|
||||
userDelta := user - lastUser
|
||||
|
||||
// Update for next call
|
||||
lastIdle = idle
|
||||
lastKernel = kernel
|
||||
lastUser = user
|
||||
|
||||
total := kernelDelta + userDelta
|
||||
if total == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
// On Windows, kernel time includes idle time; busy = total - idle
|
||||
busy := total - idleDelta
|
||||
|
||||
pct := float64(busy) / float64(total) * 100.0
|
||||
// lower bound not needed; ratios of uint64 are non-negative
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
return pct, nil
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
4
web/assets/axios/axios.min.js
vendored
4
web/assets/axios/axios.min.js
vendored
File diff suppressed because one or more lines are too long
1
web/assets/axios/axios.min.js.map
Normal file
1
web/assets/axios/axios.min.js.map
Normal file
File diff suppressed because one or more lines are too long
2
web/assets/css/custom.min.css
vendored
2
web/assets/css/custom.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -6,9 +6,12 @@ class DBInbound {
|
||||
this.up = 0;
|
||||
this.down = 0;
|
||||
this.total = 0;
|
||||
this.allTime = 0;
|
||||
this.remark = "";
|
||||
this.enable = true;
|
||||
this.expiryTime = 0;
|
||||
this.trafficReset = "never";
|
||||
this.lastTrafficResetTime = 0;
|
||||
|
||||
this.listen = "";
|
||||
this.port = 0;
|
||||
@@ -48,8 +51,8 @@ class DBInbound {
|
||||
return this.protocol === Protocols.SHADOWSOCKS;
|
||||
}
|
||||
|
||||
get isSocks() {
|
||||
return this.protocol === Protocols.SOCKS;
|
||||
get isMixed() {
|
||||
return this.protocol === Protocols.MIXED;
|
||||
}
|
||||
|
||||
get isHTTP() {
|
||||
|
||||
@@ -3,18 +3,16 @@ const Protocols = {
|
||||
VLESS: 'vless',
|
||||
TROJAN: 'trojan',
|
||||
SHADOWSOCKS: 'shadowsocks',
|
||||
DOKODEMO: 'dokodemo-door',
|
||||
SOCKS: 'socks',
|
||||
TUNNEL: 'tunnel',
|
||||
MIXED: 'mixed',
|
||||
HTTP: 'http',
|
||||
WIREGUARD: 'wireguard',
|
||||
};
|
||||
|
||||
const SSMethods = {
|
||||
AES_256_GCM: 'aes-256-gcm',
|
||||
AES_128_GCM: 'aes-128-gcm',
|
||||
CHACHA20_POLY1305: 'chacha20-poly1305',
|
||||
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
|
||||
XCHACHA20_POLY1305: 'xchacha20-poly1305',
|
||||
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
|
||||
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
|
||||
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
|
||||
@@ -559,7 +557,9 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
disableSystemRoot = false,
|
||||
enableSessionResumption = false,
|
||||
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()
|
||||
) {
|
||||
super();
|
||||
@@ -573,6 +573,8 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
this.enableSessionResumption = enableSessionResumption;
|
||||
this.certs = certificates;
|
||||
this.alpn = alpn;
|
||||
this.echServerKeys = echServerKeys;
|
||||
this.echForceQuery = echForceQuery;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@@ -592,7 +594,7 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
}
|
||||
|
||||
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(
|
||||
json.serverName,
|
||||
@@ -605,6 +607,8 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
json.enableSessionResumption,
|
||||
certs,
|
||||
json.alpn,
|
||||
json.echServerKeys,
|
||||
json.echForceQuery,
|
||||
settings,
|
||||
);
|
||||
}
|
||||
@@ -621,6 +625,8 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
enableSessionResumption: this.enableSessionResumption,
|
||||
certificates: TlsStreamSettings.toJsonArray(this.certs),
|
||||
alpn: this.alpn,
|
||||
echServerKeys: this.echServerKeys,
|
||||
echForceQuery: this.echForceQuery,
|
||||
settings: this.settings,
|
||||
};
|
||||
}
|
||||
@@ -633,7 +639,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
||||
keyFile = '',
|
||||
certificate = '',
|
||||
key = '',
|
||||
ocspStapling = 3600,
|
||||
oneTimeLoading = false,
|
||||
usage = USAGE_OPTION.ENCIPHERMENT,
|
||||
buildChain = false,
|
||||
@@ -644,7 +649,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
||||
this.keyFile = keyFile;
|
||||
this.cert = Array.isArray(certificate) ? certificate.join('\n') : certificate;
|
||||
this.key = Array.isArray(key) ? key.join('\n') : key;
|
||||
this.ocspStapling = ocspStapling;
|
||||
this.oneTimeLoading = oneTimeLoading;
|
||||
this.usage = usage;
|
||||
this.buildChain = buildChain
|
||||
@@ -656,7 +660,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
||||
true,
|
||||
json.certificateFile,
|
||||
json.keyFile, '', '',
|
||||
json.ocspStapling,
|
||||
json.oneTimeLoading,
|
||||
json.usage,
|
||||
json.buildChain,
|
||||
@@ -666,7 +669,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
||||
false, '', '',
|
||||
json.certificate.join('\n'),
|
||||
json.key.join('\n'),
|
||||
json.ocspStapling,
|
||||
json.oneTimeLoading,
|
||||
json.usage,
|
||||
json.buildChain,
|
||||
@@ -679,7 +681,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
||||
return {
|
||||
certificateFile: this.certFile,
|
||||
keyFile: this.keyFile,
|
||||
ocspStapling: this.ocspStapling,
|
||||
oneTimeLoading: this.oneTimeLoading,
|
||||
usage: this.usage,
|
||||
buildChain: this.buildChain,
|
||||
@@ -688,7 +689,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
||||
return {
|
||||
certificate: this.cert.split('\n'),
|
||||
key: this.key.split('\n'),
|
||||
ocspStapling: this.ocspStapling,
|
||||
oneTimeLoading: this.oneTimeLoading,
|
||||
usage: this.usage,
|
||||
buildChain: this.buildChain,
|
||||
@@ -701,21 +701,25 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
|
||||
constructor(
|
||||
allowInsecure = false,
|
||||
fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
|
||||
echConfigList = '',
|
||||
) {
|
||||
super();
|
||||
this.allowInsecure = allowInsecure;
|
||||
this.fingerprint = fingerprint;
|
||||
this.echConfigList = echConfigList;
|
||||
}
|
||||
static fromJson(json = {}) {
|
||||
return new TlsStreamSettings.Settings(
|
||||
json.allowInsecure,
|
||||
json.fingerprint,
|
||||
json.echConfigList,
|
||||
);
|
||||
}
|
||||
toJson() {
|
||||
return {
|
||||
allowInsecure: this.allowInsecure,
|
||||
fingerprint: this.fingerprint,
|
||||
echConfigList: this.echConfigList
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -725,25 +729,35 @@ class RealityStreamSettings extends XrayCommonClass {
|
||||
constructor(
|
||||
show = false,
|
||||
xver = 0,
|
||||
dest = 'yahoo.com:443',
|
||||
serverNames = 'yahoo.com,www.yahoo.com',
|
||||
target = '',
|
||||
serverNames = '',
|
||||
privateKey = '',
|
||||
minClient = '',
|
||||
maxClient = '',
|
||||
minClientVer = '',
|
||||
maxClientVer = '',
|
||||
maxTimediff = 0,
|
||||
shortIds = RandomUtil.randomShortIds(),
|
||||
mldsa65Seed = '',
|
||||
settings = new RealityStreamSettings.Settings()
|
||||
) {
|
||||
super();
|
||||
// If target/serverNames are not provided, use random values
|
||||
if (!target && !serverNames) {
|
||||
const randomTarget = typeof getRandomRealityTarget !== 'undefined'
|
||||
? getRandomRealityTarget()
|
||||
: { target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' };
|
||||
target = randomTarget.target;
|
||||
serverNames = randomTarget.sni;
|
||||
}
|
||||
this.show = show;
|
||||
this.xver = xver;
|
||||
this.dest = dest;
|
||||
this.target = target;
|
||||
this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames;
|
||||
this.privateKey = privateKey;
|
||||
this.minClient = minClient;
|
||||
this.maxClient = maxClient;
|
||||
this.minClientVer = minClientVer;
|
||||
this.maxClientVer = maxClientVer;
|
||||
this.maxTimediff = maxTimediff;
|
||||
this.shortIds = Array.isArray(shortIds) ? shortIds.join(",") : shortIds;
|
||||
this.mldsa65Seed = mldsa65Seed;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@@ -754,19 +768,21 @@ class RealityStreamSettings extends XrayCommonClass {
|
||||
json.settings.publicKey,
|
||||
json.settings.fingerprint,
|
||||
json.settings.serverName,
|
||||
json.settings.spiderX
|
||||
json.settings.spiderX,
|
||||
json.settings.mldsa65Verify,
|
||||
);
|
||||
}
|
||||
return new RealityStreamSettings(
|
||||
json.show,
|
||||
json.xver,
|
||||
json.dest,
|
||||
json.target,
|
||||
json.serverNames,
|
||||
json.privateKey,
|
||||
json.minClient,
|
||||
json.maxClient,
|
||||
json.minClientVer,
|
||||
json.maxClientVer,
|
||||
json.maxTimediff,
|
||||
json.shortIds,
|
||||
json.mldsa65Seed,
|
||||
settings,
|
||||
);
|
||||
}
|
||||
@@ -775,13 +791,14 @@ class RealityStreamSettings extends XrayCommonClass {
|
||||
return {
|
||||
show: this.show,
|
||||
xver: this.xver,
|
||||
dest: this.dest,
|
||||
target: this.target,
|
||||
serverNames: this.serverNames.split(","),
|
||||
privateKey: this.privateKey,
|
||||
minClient: this.minClient,
|
||||
maxClient: this.maxClient,
|
||||
minClientVer: this.minClientVer,
|
||||
maxClientVer: this.maxClientVer,
|
||||
maxTimediff: this.maxTimediff,
|
||||
shortIds: this.shortIds.split(","),
|
||||
mldsa65Seed: this.mldsa65Seed,
|
||||
settings: this.settings,
|
||||
};
|
||||
}
|
||||
@@ -792,13 +809,15 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
|
||||
publicKey = '',
|
||||
fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
|
||||
serverName = '',
|
||||
spiderX = '/'
|
||||
spiderX = '/',
|
||||
mldsa65Verify = ''
|
||||
) {
|
||||
super();
|
||||
this.publicKey = publicKey;
|
||||
this.fingerprint = fingerprint;
|
||||
this.serverName = serverName;
|
||||
this.spiderX = spiderX;
|
||||
this.mldsa65Verify = mldsa65Verify;
|
||||
}
|
||||
static fromJson(json = {}) {
|
||||
return new RealityStreamSettings.Settings(
|
||||
@@ -806,6 +825,7 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
|
||||
json.fingerprint,
|
||||
json.serverName,
|
||||
json.spiderX,
|
||||
json.mldsa65Verify
|
||||
);
|
||||
}
|
||||
toJson() {
|
||||
@@ -814,6 +834,7 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
|
||||
fingerprint: this.fingerprint,
|
||||
serverName: this.serverName,
|
||||
spiderX: this.spiderX,
|
||||
mldsa65Verify: this.mldsa65Verify
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -836,6 +857,7 @@ class SockoptStreamSettings extends XrayCommonClass {
|
||||
V6Only = false,
|
||||
tcpWindowClamp = 600,
|
||||
interfaceName = "",
|
||||
trustedXForwardedFor = [],
|
||||
) {
|
||||
super();
|
||||
this.acceptProxyProtocol = acceptProxyProtocol;
|
||||
@@ -854,6 +876,7 @@ class SockoptStreamSettings extends XrayCommonClass {
|
||||
this.V6Only = V6Only;
|
||||
this.tcpWindowClamp = tcpWindowClamp;
|
||||
this.interfaceName = interfaceName;
|
||||
this.trustedXForwardedFor = trustedXForwardedFor;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -875,11 +898,12 @@ class SockoptStreamSettings extends XrayCommonClass {
|
||||
json.V6Only,
|
||||
json.tcpWindowClamp,
|
||||
json.interface,
|
||||
json.trustedXForwardedFor || [],
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
const result = {
|
||||
acceptProxyProtocol: this.acceptProxyProtocol,
|
||||
tcpFastOpen: this.tcpFastOpen,
|
||||
mark: this.mark,
|
||||
@@ -897,6 +921,10 @@ class SockoptStreamSettings extends XrayCommonClass {
|
||||
tcpWindowClamp: this.tcpWindowClamp,
|
||||
interface: this.interfaceName,
|
||||
};
|
||||
if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {
|
||||
result.trustedXForwardedFor = this.trustedXForwardedFor;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1027,27 +1055,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 {
|
||||
constructor(
|
||||
port = RandomUtil.randomInteger(10000, 60000),
|
||||
@@ -1057,7 +1064,6 @@ class Inbound extends XrayCommonClass {
|
||||
streamSettings = new StreamSettings(),
|
||||
tag = '',
|
||||
sniffing = new Sniffing(),
|
||||
allocate = new Allocate(),
|
||||
clientStats = '',
|
||||
) {
|
||||
super();
|
||||
@@ -1068,7 +1074,6 @@ class Inbound extends XrayCommonClass {
|
||||
this.stream = streamSettings;
|
||||
this.tag = tag;
|
||||
this.sniffing = sniffing;
|
||||
this.allocate = allocate;
|
||||
this.clientStats = clientStats;
|
||||
}
|
||||
getClientStats() {
|
||||
@@ -1216,6 +1221,14 @@ class Inbound extends XrayCommonClass {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vision seed applies only when vision flow is selected
|
||||
canEnableVisionSeed() {
|
||||
if (!this.canEnableTlsFlow()) return false;
|
||||
const clients = this.settings?.vlesses;
|
||||
if (!Array.isArray(clients)) return false;
|
||||
return clients.some(c => c?.flow === TLS_FLOW_CONTROL.VISION || c?.flow === TLS_FLOW_CONTROL.VISION_UDP443);
|
||||
}
|
||||
|
||||
canEnableReality() {
|
||||
if (![Protocols.VLESS, Protocols.TROJAN].includes(this.protocol)) return false;
|
||||
return ["tcp", "http", "grpc", "xhttp"].includes(this.network);
|
||||
@@ -1233,7 +1246,6 @@ class Inbound extends XrayCommonClass {
|
||||
this.stream = new StreamSettings();
|
||||
this.tag = '';
|
||||
this.sniffing = new Sniffing();
|
||||
this.allocate = new Allocate();
|
||||
}
|
||||
|
||||
genVmessLink(address = '', port = this.port, forceTls, remark = '', clientId, security) {
|
||||
@@ -1310,6 +1322,7 @@ class Inbound extends XrayCommonClass {
|
||||
const security = forceTls == 'same' ? this.stream.security : forceTls;
|
||||
const params = new Map();
|
||||
params.set("type", this.stream.network);
|
||||
params.set("encryption", this.settings.encryption);
|
||||
switch (type) {
|
||||
case "tcp":
|
||||
const tcp = this.stream.tcp;
|
||||
@@ -1366,6 +1379,9 @@ class Inbound extends XrayCommonClass {
|
||||
if (!ObjectUtil.isEmpty(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)) {
|
||||
params.set("flow", flow);
|
||||
}
|
||||
@@ -1385,6 +1401,9 @@ class Inbound extends XrayCommonClass {
|
||||
if (!ObjectUtil.isEmpty(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)) {
|
||||
params.set("flow", flow);
|
||||
}
|
||||
@@ -1462,6 +1481,9 @@ class Inbound extends XrayCommonClass {
|
||||
if (this.stream.tls.settings.allowInsecure) {
|
||||
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)) {
|
||||
params.set("sni", this.stream.tls.sni);
|
||||
}
|
||||
@@ -1540,6 +1562,9 @@ class Inbound extends XrayCommonClass {
|
||||
if (this.stream.tls.settings.allowInsecure) {
|
||||
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)) {
|
||||
params.set("sni", this.stream.tls.sni);
|
||||
}
|
||||
@@ -1559,6 +1584,9 @@ class Inbound extends XrayCommonClass {
|
||||
if (!ObjectUtil.isEmpty(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 {
|
||||
@@ -1673,14 +1701,13 @@ class Inbound extends XrayCommonClass {
|
||||
StreamSettings.fromJson(json.streamSettings),
|
||||
json.tag,
|
||||
Sniffing.fromJson(json.sniffing),
|
||||
Allocate.fromJson(json.allocate),
|
||||
json.clientStats
|
||||
)
|
||||
}
|
||||
|
||||
toJson() {
|
||||
let streamSettings;
|
||||
if (this.canEnableStream()) {
|
||||
if (this.canEnableStream() || this.stream?.sockopt) {
|
||||
streamSettings = this.stream.toJson();
|
||||
}
|
||||
return {
|
||||
@@ -1691,7 +1718,6 @@ class Inbound extends XrayCommonClass {
|
||||
streamSettings: streamSettings,
|
||||
tag: this.tag,
|
||||
sniffing: this.sniffing.toJson(),
|
||||
allocate: this.allocate.toJson(),
|
||||
clientStats: this.clientStats
|
||||
};
|
||||
}
|
||||
@@ -1709,8 +1735,8 @@ Inbound.Settings = class extends XrayCommonClass {
|
||||
case Protocols.VLESS: return new Inbound.VLESSSettings(protocol);
|
||||
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
|
||||
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
|
||||
case Protocols.DOKODEMO: return new Inbound.DokodemoSettings(protocol);
|
||||
case Protocols.SOCKS: return new Inbound.SocksSettings(protocol);
|
||||
case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol);
|
||||
case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
|
||||
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
|
||||
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
|
||||
default: return null;
|
||||
@@ -1723,8 +1749,8 @@ Inbound.Settings = class extends XrayCommonClass {
|
||||
case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json);
|
||||
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
|
||||
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
|
||||
case Protocols.DOKODEMO: return Inbound.DokodemoSettings.fromJson(json);
|
||||
case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json);
|
||||
case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json);
|
||||
case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
|
||||
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
|
||||
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
|
||||
default: return null;
|
||||
@@ -1787,7 +1813,9 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
||||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
) {
|
||||
super();
|
||||
this.id = id;
|
||||
@@ -1801,6 +1829,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
||||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -1816,6 +1846,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
||||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
get _expiryTime() {
|
||||
@@ -1849,13 +1881,19 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
constructor(
|
||||
protocol,
|
||||
vlesses = [new Inbound.VLESSSettings.VLESS()],
|
||||
decryption = 'none',
|
||||
fallbacks = []
|
||||
decryption = "none",
|
||||
encryption = "none",
|
||||
fallbacks = [],
|
||||
selectedAuth = undefined,
|
||||
testseed = [900, 500, 900, 256],
|
||||
) {
|
||||
super(protocol);
|
||||
this.vlesses = vlesses;
|
||||
this.decryption = decryption;
|
||||
this.encryption = encryption;
|
||||
this.fallbacks = fallbacks;
|
||||
this.selectedAuth = selectedAuth;
|
||||
this.testseed = testseed;
|
||||
}
|
||||
|
||||
addFallback() {
|
||||
@@ -1866,22 +1904,54 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
this.fallbacks.splice(index, 1);
|
||||
}
|
||||
|
||||
// decryption should be set to static value
|
||||
static fromJson(json = {}) {
|
||||
return new Inbound.VLESSSettings(
|
||||
// Ensure testseed is always initialized as an array
|
||||
let testseed = [900, 500, 900, 256];
|
||||
if (json.testseed && Array.isArray(json.testseed) && json.testseed.length >= 4) {
|
||||
testseed = json.testseed;
|
||||
}
|
||||
|
||||
const obj = new Inbound.VLESSSettings(
|
||||
Protocols.VLESS,
|
||||
json.clients.map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
|
||||
json.decryption || 'none',
|
||||
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks),);
|
||||
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
|
||||
json.decryption,
|
||||
json.encryption,
|
||||
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
|
||||
json.selectedAuth,
|
||||
testseed
|
||||
);
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
const json = {
|
||||
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;
|
||||
}
|
||||
|
||||
if (this.testseed && this.testseed.length >= 4) {
|
||||
json.testseed = this.testseed;
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
@@ -1896,7 +1966,9 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
) {
|
||||
super();
|
||||
this.id = id;
|
||||
@@ -1910,6 +1982,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -1925,6 +1999,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2035,7 +2111,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
||||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
) {
|
||||
super();
|
||||
this.password = password;
|
||||
@@ -2048,6 +2126,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
||||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
@@ -2062,6 +2142,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
||||
subId: this.subId,
|
||||
comment: this.comment,
|
||||
reset: this.reset,
|
||||
created_at: this.created_at,
|
||||
updated_at: this.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2077,6 +2159,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
||||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2196,7 +2280,9 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
) {
|
||||
super();
|
||||
this.method = method;
|
||||
@@ -2210,6 +2296,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
@@ -2225,6 +2313,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||
subId: this.subId,
|
||||
comment: this.comment,
|
||||
reset: this.reset,
|
||||
created_at: this.created_at,
|
||||
updated_at: this.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2241,6 +2331,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2271,26 +2363,29 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||
|
||||
};
|
||||
|
||||
Inbound.DokodemoSettings = class extends Inbound.Settings {
|
||||
Inbound.TunnelSettings = class extends Inbound.Settings {
|
||||
constructor(
|
||||
protocol,
|
||||
address,
|
||||
port,
|
||||
portMap = [],
|
||||
network = 'tcp,udp',
|
||||
followRedirect = false
|
||||
) {
|
||||
super(protocol);
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.portMap = portMap;
|
||||
this.network = network;
|
||||
this.followRedirect = followRedirect;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new Inbound.DokodemoSettings(
|
||||
Protocols.DOKODEMO,
|
||||
return new Inbound.TunnelSettings(
|
||||
Protocols.TUNNEL,
|
||||
json.address,
|
||||
json.port,
|
||||
XrayCommonClass.toHeaders(json.portMap),
|
||||
json.network,
|
||||
json.followRedirect,
|
||||
);
|
||||
@@ -2300,14 +2395,15 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
|
||||
return {
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
portMap: XrayCommonClass.toV2Headers(this.portMap, false),
|
||||
network: this.network,
|
||||
followRedirect: this.followRedirect,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Inbound.SocksSettings = class extends Inbound.Settings {
|
||||
constructor(protocol, auth = 'password', accounts = [new Inbound.SocksSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
|
||||
Inbound.MixedSettings = class extends Inbound.Settings {
|
||||
constructor(protocol, auth = 'password', accounts = [new Inbound.MixedSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
|
||||
super(protocol);
|
||||
this.auth = auth;
|
||||
this.accounts = accounts;
|
||||
@@ -2327,11 +2423,11 @@ Inbound.SocksSettings = class extends Inbound.Settings {
|
||||
let accounts;
|
||||
if (json.auth === 'password') {
|
||||
accounts = json.accounts.map(
|
||||
account => Inbound.SocksSettings.SocksAccount.fromJson(account)
|
||||
account => Inbound.MixedSettings.SocksAccount.fromJson(account)
|
||||
)
|
||||
}
|
||||
return new Inbound.SocksSettings(
|
||||
Protocols.SOCKS,
|
||||
return new Inbound.MixedSettings(
|
||||
Protocols.MIXED,
|
||||
json.auth,
|
||||
accounts,
|
||||
json.udp,
|
||||
@@ -2348,7 +2444,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)) {
|
||||
super();
|
||||
this.user = user;
|
||||
@@ -2356,7 +2452,7 @@ Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass {
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new Inbound.SocksSettings.SocksAccount(json.user, json.pass);
|
||||
return new Inbound.MixedSettings.SocksAccount(json.user, json.pass);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ class KcpStreamSettings extends CommonClass {
|
||||
|
||||
class WsStreamSettings extends CommonClass {
|
||||
constructor(
|
||||
path = '/',
|
||||
path = '/',
|
||||
host = '',
|
||||
heartbeatPeriod = 0,
|
||||
|
||||
@@ -354,13 +354,15 @@ class TlsStreamSettings extends CommonClass {
|
||||
serverName = '',
|
||||
alpn = [],
|
||||
fingerprint = '',
|
||||
allowInsecure = false
|
||||
allowInsecure = false,
|
||||
echConfigList = '',
|
||||
) {
|
||||
super();
|
||||
this.serverName = serverName;
|
||||
this.alpn = alpn;
|
||||
this.fingerprint = fingerprint;
|
||||
this.allowInsecure = allowInsecure;
|
||||
this.echConfigList = echConfigList;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -369,6 +371,7 @@ class TlsStreamSettings extends CommonClass {
|
||||
json.alpn,
|
||||
json.fingerprint,
|
||||
json.allowInsecure,
|
||||
json.echConfigList,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -378,6 +381,7 @@ class TlsStreamSettings extends CommonClass {
|
||||
alpn: this.alpn,
|
||||
fingerprint: this.fingerprint,
|
||||
allowInsecure: this.allowInsecure,
|
||||
echConfigList: this.echConfigList
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -388,7 +392,8 @@ class RealityStreamSettings extends CommonClass {
|
||||
fingerprint = '',
|
||||
serverName = '',
|
||||
shortId = '',
|
||||
spiderX = '/'
|
||||
spiderX = '',
|
||||
mldsa65Verify = ''
|
||||
) {
|
||||
super();
|
||||
this.publicKey = publicKey;
|
||||
@@ -396,6 +401,7 @@ class RealityStreamSettings extends CommonClass {
|
||||
this.serverName = serverName;
|
||||
this.shortId = shortId
|
||||
this.spiderX = spiderX;
|
||||
this.mldsa65Verify = mldsa65Verify;
|
||||
}
|
||||
static fromJson(json = {}) {
|
||||
return new RealityStreamSettings(
|
||||
@@ -404,6 +410,7 @@ class RealityStreamSettings extends CommonClass {
|
||||
json.serverName,
|
||||
json.shortId,
|
||||
json.spiderX,
|
||||
json.mldsa65Verify
|
||||
);
|
||||
}
|
||||
toJson() {
|
||||
@@ -413,6 +420,7 @@ class RealityStreamSettings extends CommonClass {
|
||||
serverName: this.serverName,
|
||||
shortId: this.shortId,
|
||||
spiderX: this.spiderX,
|
||||
mldsa65Verify: this.mldsa65Verify
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -424,6 +432,7 @@ class SockoptStreamSettings extends CommonClass {
|
||||
tcpMptcp = false,
|
||||
penetrate = false,
|
||||
addressPortStrategy = Address_Port_Strategy.NONE,
|
||||
trustedXForwardedFor = [],
|
||||
) {
|
||||
super();
|
||||
this.dialerProxy = dialerProxy;
|
||||
@@ -432,6 +441,7 @@ class SockoptStreamSettings extends CommonClass {
|
||||
this.tcpMptcp = tcpMptcp;
|
||||
this.penetrate = penetrate;
|
||||
this.addressPortStrategy = addressPortStrategy;
|
||||
this.trustedXForwardedFor = trustedXForwardedFor;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -442,12 +452,13 @@ class SockoptStreamSettings extends CommonClass {
|
||||
json.tcpKeepAliveInterval,
|
||||
json.tcpMptcp,
|
||||
json.penetrate,
|
||||
json.addressPortStrategy
|
||||
json.addressPortStrategy,
|
||||
json.trustedXForwardedFor || []
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
const result = {
|
||||
dialerProxy: this.dialerProxy,
|
||||
tcpFastOpen: this.tcpFastOpen,
|
||||
tcpKeepAliveInterval: this.tcpKeepAliveInterval,
|
||||
@@ -455,6 +466,10 @@ class SockoptStreamSettings extends CommonClass {
|
||||
penetrate: this.penetrate,
|
||||
addressPortStrategy: this.addressPortStrategy
|
||||
};
|
||||
if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {
|
||||
result.trustedXForwardedFor = this.trustedXForwardedFor;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,6 +621,13 @@ class Outbound extends CommonClass {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vision seed applies only when vision flow is selected
|
||||
canEnableVisionSeed() {
|
||||
if (!this.canEnableTlsFlow()) return false;
|
||||
const flow = this.settings?.flow;
|
||||
return flow === TLS_FLOW_CONTROL.VISION || flow === TLS_FLOW_CONTROL.VISION_UDP443;
|
||||
}
|
||||
|
||||
canEnableReality() {
|
||||
if (![Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
|
||||
return ["tcp", "http", "grpc", "xhttp"].includes(this.stream.network);
|
||||
@@ -639,10 +661,6 @@ class Outbound extends CommonClass {
|
||||
].includes(this.protocol);
|
||||
}
|
||||
|
||||
hasVnext() {
|
||||
return [Protocols.VMess, Protocols.VLESS].includes(this.protocol);
|
||||
}
|
||||
|
||||
hasServers() {
|
||||
return [Protocols.Trojan, Protocols.Shadowsocks, Protocols.Socks, Protocols.HTTP].includes(this.protocol);
|
||||
}
|
||||
@@ -682,13 +700,15 @@ class Outbound extends CommonClass {
|
||||
if (this.stream?.sockopt)
|
||||
stream = { sockopt: this.stream.sockopt.toJson() };
|
||||
}
|
||||
let settingsOut = this.settings instanceof CommonClass ? this.settings.toJson() : this.settings;
|
||||
return {
|
||||
tag: this.tag == '' ? undefined : this.tag,
|
||||
protocol: this.protocol,
|
||||
settings: this.settings instanceof CommonClass ? this.settings.toJson() : this.settings,
|
||||
streamSettings: stream,
|
||||
sendThrough: this.sendThrough != "" ? this.sendThrough : undefined,
|
||||
mux: this.mux?.enabled ? this.mux : undefined,
|
||||
settings: settingsOut,
|
||||
// Only include tag, streamSettings, sendThrough, mux if present and not empty
|
||||
...(this.tag ? { tag: this.tag } : {}),
|
||||
...(stream ? { streamSettings: stream } : {}),
|
||||
...(this.sendThrough ? { sendThrough: this.sendThrough } : {}),
|
||||
...(this.mux?.enabled ? { mux: this.mux } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -778,7 +798,8 @@ class Outbound extends CommonClass {
|
||||
let alpn = url.searchParams.get('alpn');
|
||||
let allowInsecure = url.searchParams.get('allowInsecure');
|
||||
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') {
|
||||
@@ -787,7 +808,8 @@ class Outbound extends CommonClass {
|
||||
let sni = url.searchParams.get('sni') ?? '';
|
||||
let sid = url.searchParams.get('sid') ?? '';
|
||||
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+)(.*)$/;
|
||||
@@ -803,7 +825,7 @@ class Outbound extends CommonClass {
|
||||
var settings;
|
||||
switch (protocol) {
|
||||
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;
|
||||
case Protocols.Trojan:
|
||||
settings = new Outbound.TrojanSettings(address, port, userData);
|
||||
@@ -898,7 +920,7 @@ Outbound.FreedomSettings = class extends CommonClass {
|
||||
toJson() {
|
||||
return {
|
||||
domainStrategy: ObjectUtil.isEmpty(this.domainStrategy) ? undefined : this.domainStrategy,
|
||||
redirect: ObjectUtil.isEmpty(this.redirect) ? undefined: this.redirect,
|
||||
redirect: ObjectUtil.isEmpty(this.redirect) ? undefined : this.redirect,
|
||||
fragment: Object.keys(this.fragment).length === 0 ? undefined : this.fragment,
|
||||
noises: this.noises.length === 0 ? undefined : Outbound.FreedomSettings.Noise.toJsonArray(this.noises),
|
||||
};
|
||||
@@ -909,12 +931,14 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
|
||||
constructor(
|
||||
packets = '1-3',
|
||||
length = '',
|
||||
interval = ''
|
||||
interval = '',
|
||||
maxSplit = ''
|
||||
) {
|
||||
super();
|
||||
this.packets = packets;
|
||||
this.length = length;
|
||||
this.interval = interval;
|
||||
this.maxSplit = maxSplit;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -922,6 +946,7 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
|
||||
json.packets,
|
||||
json.length,
|
||||
json.interval,
|
||||
json.maxSplit
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -930,12 +955,14 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
|
||||
constructor(
|
||||
type = 'rand',
|
||||
packet = '10-20',
|
||||
delay = '10-16'
|
||||
delay = '10-16',
|
||||
applyTo = 'ip'
|
||||
) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.packet = packet;
|
||||
this.delay = delay;
|
||||
this.applyTo = applyTo;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -943,6 +970,7 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
|
||||
json.type,
|
||||
json.packet,
|
||||
json.delay,
|
||||
json.applyTo
|
||||
);
|
||||
}
|
||||
|
||||
@@ -951,6 +979,7 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
|
||||
type: this.type,
|
||||
packet: this.packet,
|
||||
delay: this.delay,
|
||||
applyTo: this.applyTo
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -978,7 +1007,7 @@ Outbound.DNSSettings = class extends CommonClass {
|
||||
network = 'udp',
|
||||
address = '',
|
||||
port = 53,
|
||||
nonIPQuery = 'drop',
|
||||
nonIPQuery = 'reject',
|
||||
blockTypes = []
|
||||
) {
|
||||
super();
|
||||
@@ -1009,13 +1038,16 @@ Outbound.VmessSettings = class extends CommonClass {
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
if (ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VmessSettings();
|
||||
return new Outbound.VmessSettings(
|
||||
json.vnext[0].address,
|
||||
json.vnext[0].port,
|
||||
json.vnext[0].users[0].id,
|
||||
json.vnext[0].users[0].security,
|
||||
);
|
||||
if (!ObjectUtil.isArrEmpty(json.vnext)) {
|
||||
const v = json.vnext[0] || {};
|
||||
const u = ObjectUtil.isArrEmpty(v.users) ? {} : v.users[0];
|
||||
return new Outbound.VmessSettings(
|
||||
v.address,
|
||||
v.port,
|
||||
u.id,
|
||||
u.security,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toJson() {
|
||||
@@ -1023,40 +1055,54 @@ Outbound.VmessSettings = class extends CommonClass {
|
||||
vnext: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
users: [{ id: this.id, security: this.security }],
|
||||
}],
|
||||
users: [{
|
||||
id: this.id,
|
||||
security: this.security
|
||||
}]
|
||||
}]
|
||||
};
|
||||
}
|
||||
};
|
||||
Outbound.VLESSSettings = class extends CommonClass {
|
||||
constructor(address, port, id, flow, encryption = 'none') {
|
||||
constructor(address, port, id, flow, encryption, testpre = 0, testseed = [900, 500, 900, 256]) {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.id = id;
|
||||
this.flow = flow;
|
||||
this.encryption = encryption
|
||||
this.encryption = encryption;
|
||||
this.testpre = testpre;
|
||||
this.testseed = testseed;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
if (ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VLESSSettings();
|
||||
if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VLESSSettings();
|
||||
return new Outbound.VLESSSettings(
|
||||
json.vnext[0].address,
|
||||
json.vnext[0].port,
|
||||
json.vnext[0].users[0].id,
|
||||
json.vnext[0].users[0].flow,
|
||||
json.vnext[0].users[0].encryption,
|
||||
json.address,
|
||||
json.port,
|
||||
json.id,
|
||||
json.flow,
|
||||
json.encryption,
|
||||
json.testpre || 0,
|
||||
json.testseed && json.testseed.length >= 4 ? json.testseed : [900, 500, 900, 256]
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
vnext: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
users: [{ id: this.id, flow: this.flow, encryption: 'none', }],
|
||||
}],
|
||||
const result = {
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
id: this.id,
|
||||
flow: this.flow,
|
||||
encryption: this.encryption,
|
||||
};
|
||||
if (this.testpre > 0) {
|
||||
result.testpre = this.testpre;
|
||||
}
|
||||
if (this.testseed && this.testseed.length >= 4) {
|
||||
result.testseed = this.testseed;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
Outbound.TrojanSettings = class extends CommonClass {
|
||||
|
||||
31
web/assets/js/model/reality_targets.js
Normal file
31
web/assets/js/model/reality_targets.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// List of popular services for VLESS Reality Target/SNI randomization
|
||||
const REALITY_TARGETS = [
|
||||
{ target: 'www.icloud.com:443', sni: 'www.icloud.com,icloud.com' },
|
||||
{ target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' },
|
||||
{ target: 'www.tesla.com:443', sni: 'www.tesla.com,tesla.com' },
|
||||
{ target: 'www.sony.com:443', sni: 'www.sony.com,sony.com' },
|
||||
{ target: 'www.nvidia.com:443', sni: 'www.nvidia.com,nvidia.com' },
|
||||
{ target: 'www.amd.com:443', sni: 'www.amd.com,amd.com' },
|
||||
{ target: 'azure.microsoft.com:443', sni: 'azure.microsoft.com,www.azure.com' },
|
||||
{ target: 'aws.amazon.com:443', sni: 'aws.amazon.com,amazon.com' },
|
||||
{ target: 'www.bing.com:443', sni: 'www.bing.com,bing.com' },
|
||||
{ target: 'www.oracle.com:443', sni: 'www.oracle.com,oracle.com' },
|
||||
{ target: 'www.intel.com:443', sni: 'www.intel.com,intel.com' },
|
||||
{ target: 'www.microsoft.com:443', sni: 'www.microsoft.com,microsoft.com' },
|
||||
{ target: 'www.amazon.com:443', sni: 'www.amazon.com,amazon.com' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns a random Reality target configuration from the predefined list
|
||||
* @returns {Object} Object with target and sni properties
|
||||
*/
|
||||
function getRandomRealityTarget() {
|
||||
const randomIndex = Math.floor(Math.random() * REALITY_TARGETS.length);
|
||||
const selected = REALITY_TARGETS[randomIndex];
|
||||
// Return a copy to avoid reference issues
|
||||
return {
|
||||
target: selected.target,
|
||||
sni: selected.sni
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ class AllSetting {
|
||||
this.webCertFile = "";
|
||||
this.webKeyFile = "";
|
||||
this.webBasePath = "/";
|
||||
this.sessionMaxAge = 60;
|
||||
this.pageSize = 50;
|
||||
this.sessionMaxAge = 360;
|
||||
this.pageSize = 25;
|
||||
this.expireDiff = 0;
|
||||
this.trafficDiff = 0;
|
||||
this.remarkModel = "-ieo";
|
||||
@@ -26,7 +26,8 @@ class AllSetting {
|
||||
this.twoFactorEnable = false;
|
||||
this.twoFactorToken = "";
|
||||
this.xrayTemplateConfig = "";
|
||||
this.subEnable = false;
|
||||
this.subEnable = true;
|
||||
this.subJsonEnable = false;
|
||||
this.subTitle = "";
|
||||
this.subListen = "";
|
||||
this.subPort = 2096;
|
||||
@@ -49,6 +50,28 @@ class AllSetting {
|
||||
|
||||
this.timeLocation = "Local";
|
||||
|
||||
// LDAP settings
|
||||
this.ldapEnable = false;
|
||||
this.ldapHost = "";
|
||||
this.ldapPort = 389;
|
||||
this.ldapUseTLS = false;
|
||||
this.ldapBindDN = "";
|
||||
this.ldapPassword = "";
|
||||
this.ldapBaseDN = "";
|
||||
this.ldapUserFilter = "(objectClass=person)";
|
||||
this.ldapUserAttr = "mail";
|
||||
this.ldapVlessField = "vless_enabled";
|
||||
this.ldapSyncCron = "@every 1m";
|
||||
this.ldapFlagField = "";
|
||||
this.ldapTruthyValues = "true,1,yes,on";
|
||||
this.ldapInvertFlag = false;
|
||||
this.ldapInboundTags = "";
|
||||
this.ldapAutoCreate = false;
|
||||
this.ldapAutoDelete = false;
|
||||
this.ldapDefaultTotalGB = 0;
|
||||
this.ldapDefaultExpiryDays = 0;
|
||||
this.ldapDefaultLimitIP = 0;
|
||||
|
||||
if (data == null) {
|
||||
return
|
||||
}
|
||||
|
||||
160
web/assets/js/subscription.js
Normal file
160
web/assets/js/subscription.js
Normal file
@@ -0,0 +1,160 @@
|
||||
(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://')) {
|
||||
const hashIdx = link.indexOf('#');
|
||||
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
|
||||
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');
|
||||
}
|
||||
const at = link.indexOf('@');
|
||||
const protSep = link.indexOf('://');
|
||||
if (at !== -1 && protSep !== -1) return link.substring(protSep + 3, at);
|
||||
} else if (link.startsWith('ss://')) {
|
||||
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();
|
||||
const tpl = document.getElementById('subscription-data');
|
||||
const sj = tpl ? tpl.getAttribute('data-subjson-url') : '';
|
||||
if (sj) this.app.subJsonUrl = sj;
|
||||
drawQR(this.app.subUrl);
|
||||
try {
|
||||
const elJson = document.getElementById('qrcode-subjson');
|
||||
if (elJson && this.app.subJsonUrl) {
|
||||
new QRious({ element: elJson, value: this.app.subJsonUrl, size: 220 });
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
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;
|
||||
},
|
||||
shadowrocketUrl() {
|
||||
const rawUrl = this.app.subUrl + '?flag=shadowrocket';
|
||||
const base64Url = btoa(rawUrl);
|
||||
const remark = encodeURIComponent(this.app.sId || 'Subscription');
|
||||
return `shadowrocket://add/sub/${base64Url}?remark=${remark}`;
|
||||
},
|
||||
v2boxUrl() {
|
||||
return `v2box://install-sub?url=${encodeURIComponent(this.app.subUrl)}&name=${encodeURIComponent(this.app.sId)}`;
|
||||
},
|
||||
streisandUrl() {
|
||||
return `streisand://import/${encodeURIComponent(this.app.subUrl)}`;
|
||||
},
|
||||
v2raytunUrl() {
|
||||
return this.app.subUrl;
|
||||
},
|
||||
npvtunUrl() {
|
||||
return this.app.subUrl;
|
||||
},
|
||||
happUrl() {
|
||||
return `happ://add/${encodeURIComponent(this.app.subUrl)}`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
renderLink,
|
||||
copy,
|
||||
open,
|
||||
linkName,
|
||||
i18nLabel(key) {
|
||||
return '{{ i18n "' + key + '" }}';
|
||||
},
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -1,151 +0,0 @@
|
||||
const oneMinute = 1000 * 60; // MilliseConds in a Minute
|
||||
const oneHour = oneMinute * 60; // The milliseconds of one hour
|
||||
const oneDay = oneHour * 24; // The Number of MilliseConds A Day
|
||||
const oneWeek = oneDay * 7; // The milliseconds per week
|
||||
const oneMonth = oneDay * 30; // The milliseconds of a month
|
||||
|
||||
/**
|
||||
* Decrease according to the number of days
|
||||
*
|
||||
* @param days to reduce the number of days to be reduced
|
||||
*/
|
||||
Date.prototype.minusDays = function (days) {
|
||||
return this.minusMillis(oneDay * days);
|
||||
};
|
||||
|
||||
/**
|
||||
* Increase according to the number of days
|
||||
*
|
||||
* @param days The number of days to be increased
|
||||
*/
|
||||
Date.prototype.plusDays = function (days) {
|
||||
return this.plusMillis(oneDay * days);
|
||||
};
|
||||
|
||||
/**
|
||||
* A few
|
||||
*
|
||||
* @param hours to be reduced
|
||||
*/
|
||||
Date.prototype.minusHours = function (hours) {
|
||||
return this.minusMillis(oneHour * hours);
|
||||
};
|
||||
|
||||
/**
|
||||
* Increase hourly
|
||||
*
|
||||
* @param hours to increase the number of hours
|
||||
*/
|
||||
Date.prototype.plusHours = function (hours) {
|
||||
return this.plusMillis(oneHour * hours);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make reduction in minutes
|
||||
*
|
||||
* @param minutes to reduce the number of minutes
|
||||
*/
|
||||
Date.prototype.minusMinutes = function (minutes) {
|
||||
return this.minusMillis(oneMinute * minutes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add in minutes
|
||||
*
|
||||
* @param minutes to increase the number of minutes
|
||||
*/
|
||||
Date.prototype.plusMinutes = function (minutes) {
|
||||
return this.plusMillis(oneMinute * minutes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Decrease in milliseconds
|
||||
*
|
||||
* @param millis to reduce the milliseconds
|
||||
*/
|
||||
Date.prototype.minusMillis = function(millis) {
|
||||
let time = this.getTime() - millis;
|
||||
let newDate = new Date();
|
||||
newDate.setTime(time);
|
||||
return newDate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add in milliseconds to increase
|
||||
*
|
||||
* @param millis to increase the milliseconds to increase
|
||||
*/
|
||||
Date.prototype.plusMillis = function(millis) {
|
||||
let time = this.getTime() + millis;
|
||||
let newDate = new Date();
|
||||
newDate.setTime(time);
|
||||
return newDate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Setting time is 00: 00: 00.000 on the day
|
||||
*/
|
||||
Date.prototype.setMinTime = function () {
|
||||
this.setHours(0);
|
||||
this.setMinutes(0);
|
||||
this.setSeconds(0);
|
||||
this.setMilliseconds(0);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Setting time is 23: 59: 59.999 on the same day
|
||||
*/
|
||||
Date.prototype.setMaxTime = function () {
|
||||
this.setHours(23);
|
||||
this.setMinutes(59);
|
||||
this.setSeconds(59);
|
||||
this.setMilliseconds(999);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatting date
|
||||
*/
|
||||
Date.prototype.formatDate = function () {
|
||||
return this.getFullYear() + "-" + NumberFormatter.addZero(this.getMonth() + 1) + "-" + NumberFormatter.addZero(this.getDate());
|
||||
};
|
||||
|
||||
/**
|
||||
* Format time
|
||||
*/
|
||||
Date.prototype.formatTime = function () {
|
||||
return NumberFormatter.addZero(this.getHours()) + ":" + NumberFormatter.addZero(this.getMinutes()) + ":" + NumberFormatter.addZero(this.getSeconds());
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatting date plus time
|
||||
*
|
||||
* @param split Date and time separation symbols, default is a space
|
||||
*/
|
||||
Date.prototype.formatDateTime = function (split = ' ') {
|
||||
return this.formatDate() + split + this.formatTime();
|
||||
};
|
||||
|
||||
class DateUtil {
|
||||
// String to date object
|
||||
static parseDate(str) {
|
||||
return new Date(str.replace(/-/g, '/'));
|
||||
}
|
||||
|
||||
static formatMillis(millis) {
|
||||
return moment(millis).format('YYYY-M-D H:m:s');
|
||||
}
|
||||
|
||||
static firstDayOfMonth() {
|
||||
const date = new Date();
|
||||
date.setDate(1);
|
||||
date.setMinTime();
|
||||
return date;
|
||||
}
|
||||
|
||||
static convertToJalalian(date) {
|
||||
return date && moment.isMoment(date) ? date.format('jYYYY/jMM/jDD HH:mm:ss') : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -316,15 +316,13 @@ class ObjectUtil {
|
||||
}
|
||||
|
||||
static equals(a, b) {
|
||||
for (const key in a) {
|
||||
if (!a.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
if (!b.hasOwnProperty(key)) {
|
||||
return false;
|
||||
} else if (a[key] !== b[key]) {
|
||||
return false;
|
||||
}
|
||||
// shallow, symmetric comparison so newly added fields also affect equality
|
||||
const aKeys = Object.keys(a);
|
||||
const bKeys = Object.keys(b);
|
||||
if (aKeys.length !== bKeys.length) return false;
|
||||
for (const key of aKeys) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
||||
if (a[key] !== b[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -884,4 +882,35 @@ class FileManager {
|
||||
|
||||
link.remove();
|
||||
}
|
||||
}
|
||||
|
||||
class IntlUtil {
|
||||
static formatDate(date) {
|
||||
const language = LanguageManager.getLanguage()
|
||||
|
||||
let intlOptions = {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric"
|
||||
}
|
||||
|
||||
const intl = new Intl.DateTimeFormat(
|
||||
language,
|
||||
intlOptions
|
||||
)
|
||||
|
||||
return intl.format(new Date(date))
|
||||
}
|
||||
static formatRelativeTime(date) {
|
||||
const language = LanguageManager.getLanguage()
|
||||
const now = new Date()
|
||||
|
||||
const diff = Math.round((date - now) / (1000 * 60 * 60 * 24))
|
||||
const formatter = new Intl.RelativeTimeFormat(language, { numeric: 'auto' })
|
||||
|
||||
return formatter.format(diff, 'day');
|
||||
}
|
||||
}
|
||||
145
web/assets/js/websocket.js
Normal file
145
web/assets/js/websocket.js
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* WebSocket client for real-time updates
|
||||
*/
|
||||
class WebSocketClient {
|
||||
constructor(basePath = '') {
|
||||
this.basePath = basePath;
|
||||
this.ws = null;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 10;
|
||||
this.reconnectDelay = 1000;
|
||||
this.listeners = new Map();
|
||||
this.isConnected = false;
|
||||
this.shouldReconnect = true;
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
// Ensure basePath ends with '/' for proper URL construction
|
||||
let basePath = this.basePath || '';
|
||||
if (basePath && !basePath.endsWith('/')) {
|
||||
basePath += '/';
|
||||
}
|
||||
const wsUrl = `${protocol}//${window.location.host}${basePath}ws`;
|
||||
|
||||
console.log('WebSocket connecting to:', wsUrl, 'basePath:', this.basePath);
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.emit('connected');
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
// Validate message size (prevent memory issues)
|
||||
const maxMessageSize = 10 * 1024 * 1024; // 10MB
|
||||
if (event.data && event.data.length > maxMessageSize) {
|
||||
console.error('WebSocket message too large:', event.data.length, 'bytes');
|
||||
this.ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const message = JSON.parse(event.data);
|
||||
if (!message || typeof message !== 'object') {
|
||||
console.error('Invalid WebSocket message format');
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleMessage(message);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse WebSocket message:', e);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
this.emit('error', error);
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
this.isConnected = false;
|
||||
this.emit('disconnected');
|
||||
|
||||
if (this.shouldReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectAttempts++;
|
||||
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
||||
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
||||
setTimeout(() => this.connect(), delay);
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Failed to create WebSocket connection:', e);
|
||||
this.emit('error', e);
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(message) {
|
||||
const { type, payload, time } = message;
|
||||
|
||||
// Emit to specific type listeners
|
||||
this.emit(type, payload, time);
|
||||
|
||||
// Emit to all listeners
|
||||
this.emit('message', { type, payload, time });
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, []);
|
||||
}
|
||||
this.listeners.get(event).push(callback);
|
||||
}
|
||||
|
||||
off(event, callback) {
|
||||
if (!this.listeners.has(event)) {
|
||||
return;
|
||||
}
|
||||
const callbacks = this.listeners.get(event);
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
emit(event, ...args) {
|
||||
if (this.listeners.has(event)) {
|
||||
this.listeners.get(event).forEach(callback => {
|
||||
try {
|
||||
callback(...args);
|
||||
} catch (e) {
|
||||
console.error('Error in WebSocket event handler:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.shouldReconnect = false;
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
send(data) {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify(data));
|
||||
} else {
|
||||
console.warn('WebSocket is not connected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create global WebSocket client instance
|
||||
// Safely get basePath from global scope (defined in page.html)
|
||||
window.wsClient = new WebSocketClient(typeof basePath !== 'undefined' ? basePath : '');
|
||||
1
web/assets/moment/moment.min.js.map
Normal file
1
web/assets/moment/moment.min.js.map
Normal file
File diff suppressed because one or more lines are too long
34
web/assets/otpauth/otpauth.umd.min.js
vendored
34
web/assets/otpauth/otpauth.umd.min.js
vendored
@@ -1,19 +1,19 @@
|
||||
//! otpauth 9.4.0 | (c) Héctor Molinero Fernández | MIT | https://github.com/hectorm/otpauth
|
||||
//! noble-hashes 1.7.1 | (c) Paul Miller | MIT | https://github.com/paulmillr/noble-hashes
|
||||
//! otpauth 9.4.1 | (c) Héctor Molinero Fernández | MIT | https://github.com/hectorm/otpauth
|
||||
//! noble-hashes 1.8.0 | (c) Paul Miller | MIT | https://github.com/paulmillr/noble-hashes
|
||||
/// <reference types="./otpauth.d.ts" />
|
||||
// @ts-nocheck
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).OTPAuth={})}(this,(function(t){"use strict";function e(t){if(!Number.isSafeInteger(t)||t<0)throw new Error("positive integer expected, got "+t)}function s(t,...e){if(!((s=t)instanceof Uint8Array||ArrayBuffer.isView(s)&&"Uint8Array"===s.constructor.name))throw new Error("Uint8Array expected");var s;if(e.length>0&&!e.includes(t.length))throw new Error("Uint8Array expected of length "+e+", got length="+t.length)}function i(t,e=!0){if(t.destroyed)throw new Error("Hash instance has been destroyed");if(e&&t.finished)throw new Error("Hash#digest() has already been called")}function r(t,e){s(t);const i=e.outputLen;if(t.length<i)throw new Error("digestInto() expects output buffer of length at least "+i)}function n(t){return new DataView(t.buffer,t.byteOffset,t.byteLength)}function o(t,e){return t<<32-e|t>>>e}function h(t,e){return t<<e|t>>>32-e>>>0}const a=(()=>68===new Uint8Array(new Uint32Array([287454020]).buffer)[0])();function l(t){for(let s=0;s<t.length;s++)t[s]=(e=t[s])<<24&4278190080|e<<8&16711680|e>>>8&65280|e>>>24&255;var e}function c(t){return"string"==typeof t&&(t=function(t){if("string"!=typeof t)throw new Error("utf8ToBytes expected string, got "+typeof t);return new Uint8Array((new TextEncoder).encode(t))}(t)),s(t),t}class u{clone(){return this._cloneInto()}}function d(t){const e=e=>t().update(c(e)).digest(),s=t();return e.outputLen=s.outputLen,e.blockLen=s.blockLen,e.create=()=>t(),e}class f extends u{update(t){return i(this),this.iHash.update(t),this}digestInto(t){i(this),s(t,this.outputLen),this.finished=!0,this.iHash.digestInto(t),this.oHash.update(t),this.oHash.digestInto(t),this.destroy()}digest(){const t=new Uint8Array(this.oHash.outputLen);return this.digestInto(t),t}_cloneInto(t){t||(t=Object.create(Object.getPrototypeOf(this),{}));const{oHash:e,iHash:s,finished:i,destroyed:r,blockLen:n,outputLen:o}=this
|
||||
;return t.finished=i,t.destroyed=r,t.blockLen=n,t.outputLen=o,t.oHash=e._cloneInto(t.oHash),t.iHash=s._cloneInto(t.iHash),t}destroy(){this.destroyed=!0,this.oHash.destroy(),this.iHash.destroy()}constructor(t,s){super(),this.finished=!1,this.destroyed=!1,function(t){if("function"!=typeof t||"function"!=typeof t.create)throw new Error("Hash should be wrapped by utils.wrapConstructor");e(t.outputLen),e(t.blockLen)}(t);const i=c(s);if(this.iHash=t.create(),"function"!=typeof this.iHash.update)throw new Error("Expected instance of class which extends utils.Hash");this.blockLen=this.iHash.blockLen,this.outputLen=this.iHash.outputLen;const r=this.blockLen,n=new Uint8Array(r);n.set(i.length>r?t.create().update(i).digest():i);for(let t=0;t<n.length;t++)n[t]^=54;this.iHash.update(n),this.oHash=t.create();for(let t=0;t<n.length;t++)n[t]^=106;this.oHash.update(n),n.fill(0)}}const b=(t,e,s)=>new f(t,e).update(s).digest();function g(t,e,s){return t&e^~t&s}function p(t,e,s){return t&e^t&s^e&s}b.create=(t,e)=>new f(t,e);class w extends u{update(t){i(this);const{view:e,buffer:s,blockLen:r}=this,o=(t=c(t)).length;for(let i=0;i<o;){const h=Math.min(r-this.pos,o-i);if(h!==r)s.set(t.subarray(i,i+h),this.pos),this.pos+=h,i+=h,this.pos===r&&(this.process(e,0),this.pos=0);else{const e=n(t);for(;r<=o-i;i+=r)this.process(e,i)}}return this.length+=t.length,this.roundClean(),this}digestInto(t){i(this),r(t,this),this.finished=!0;const{buffer:e,view:s,blockLen:o,isLE:h}=this;let{pos:a}=this;e[a++]=128,this.buffer.subarray(a).fill(0),this.padOffset>o-a&&(this.process(s,0),a=0);for(let t=a;t<o;t++)e[t]=0;!function(t,e,s,i){if("function"==typeof t.setBigUint64)return t.setBigUint64(e,s,i);const r=BigInt(32),n=BigInt(4294967295),o=Number(s>>r&n),h=Number(s&n),a=i?4:0,l=i?0:4;t.setUint32(e+a,o,i),t.setUint32(e+l,h,i)}(s,o-8,BigInt(8*this.length),h),this.process(s,0);const l=n(t),c=this.outputLen;if(c%4)throw new Error("_sha2: outputLen should be aligned to 32bit");const u=c/4,d=this.get()
|
||||
;if(u>d.length)throw new Error("_sha2: outputLen bigger than state");for(let t=0;t<u;t++)l.setUint32(4*t,d[t],h)}digest(){const{buffer:t,outputLen:e}=this;this.digestInto(t);const s=t.slice(0,e);return this.destroy(),s}_cloneInto(t){t||(t=new this.constructor),t.set(...this.get());const{blockLen:e,buffer:s,length:i,finished:r,destroyed:n,pos:o}=this;return t.length=i,t.pos=o,t.finished=r,t.destroyed=n,i%e&&t.buffer.set(s),t}constructor(t,e,s,i){super(),this.blockLen=t,this.outputLen=e,this.padOffset=s,this.isLE=i,this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.buffer=new Uint8Array(t),this.view=n(this.buffer)}}const y=new Uint32Array([1732584193,4023233417,2562383102,271733878,3285377520]),x=new Uint32Array(80);class A extends w{get(){const{A:t,B:e,C:s,D:i,E:r}=this;return[t,e,s,i,r]}set(t,e,s,i,r){this.A=0|t,this.B=0|e,this.C=0|s,this.D=0|i,this.E=0|r}process(t,e){for(let s=0;s<16;s++,e+=4)x[s]=t.getUint32(e,!1);for(let t=16;t<80;t++)x[t]=h(x[t-3]^x[t-8]^x[t-14]^x[t-16],1);let{A:s,B:i,C:r,D:n,E:o}=this;for(let t=0;t<80;t++){let e,a;t<20?(e=g(i,r,n),a=1518500249):t<40?(e=i^r^n,a=1859775393):t<60?(e=p(i,r,n),a=2400959708):(e=i^r^n,a=3395469782);const l=h(s,5)+e+o+a+x[t]|0;o=n,n=r,r=h(i,30),i=s,s=l}s=s+this.A|0,i=i+this.B|0,r=r+this.C|0,n=n+this.D|0,o=o+this.E|0,this.set(s,i,r,n,o)}roundClean(){x.fill(0)}destroy(){this.set(0,0,0,0,0),this.buffer.fill(0)}constructor(){super(64,20,8,!1),this.A=0|y[0],this.B=0|y[1],this.C=0|y[2],this.D=0|y[3],this.E=0|y[4]}}
|
||||
const m=d((()=>new A)),H=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),L=new Uint32Array([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),I=new Uint32Array(64);class S extends w{get(){const{A:t,B:e,C:s,D:i,E:r,F:n,G:o,H:h}=this;return[t,e,s,i,r,n,o,h]}set(t,e,s,i,r,n,o,h){this.A=0|t,this.B=0|e,this.C=0|s,this.D=0|i,this.E=0|r,this.F=0|n,this.G=0|o,this.H=0|h}process(t,e){for(let s=0;s<16;s++,e+=4)I[s]=t.getUint32(e,!1);for(let t=16;t<64;t++){const e=I[t-15],s=I[t-2],i=o(e,7)^o(e,18)^e>>>3,r=o(s,17)^o(s,19)^s>>>10;I[t]=r+I[t-7]+i+I[t-16]|0}let{A:s,B:i,C:r,D:n,E:h,F:a,G:l,H:c}=this;for(let t=0;t<64;t++){const e=c+(o(h,6)^o(h,11)^o(h,25))+g(h,a,l)+H[t]+I[t]|0,u=(o(s,2)^o(s,13)^o(s,22))+p(s,i,r)|0;c=l,l=a,a=h,h=n+e|0,n=r,r=i,i=s,s=e+u|0}s=s+this.A|0,i=i+this.B|0,r=r+this.C|0,n=n+this.D|0,h=h+this.E|0,a=a+this.F|0,l=l+this.G|0,c=c+this.H|0,this.set(s,i,r,n,h,a,l,c)}roundClean(){I.fill(0)}destroy(){this.set(0,0,0,0,0,0,0,0),this.buffer.fill(0)}constructor(){super(64,32,8,!1),this.A=0|L[0],this.B=0|L[1],this.C=0|L[2],this.D=0|L[3],this.E=0|L[4],this.F=0|L[5],this.G=0|L[6],this.H=0|L[7]}}class B extends S{constructor(){super(),this.A=-1056596264,this.B=914150663,this.C=812702999,this.D=-150054599,this.E=-4191439,this.F=1750603025,this.G=1694076839,this.H=-1090891868,this.outputLen=28}}
|
||||
const E=d((()=>new S)),U=d((()=>new B)),C=BigInt(2**32-1),O=BigInt(32);function v(t,e=!1){return e?{h:Number(t&C),l:Number(t>>O&C)}:{h:0|Number(t>>O&C),l:0|Number(t&C)}}function k(t,e=!1){let s=new Uint32Array(t.length),i=new Uint32Array(t.length);for(let r=0;r<t.length;r++){const{h:n,l:o}=v(t[r],e);[s[r],i[r]]=[n,o]}return[s,i]}const T=(t,e,s)=>t<<s|e>>>32-s,$=(t,e,s)=>e<<s|t>>>32-s,D=(t,e,s)=>e<<s-32|t>>>64-s,_=(t,e,s)=>t<<s-32|e>>>64-s,F={fromBig:v,split:k,toBig:(t,e)=>BigInt(t>>>0)<<O|BigInt(e>>>0),shrSH:(t,e,s)=>t>>>s,shrSL:(t,e,s)=>t<<32-s|e>>>s,rotrSH:(t,e,s)=>t>>>s|e<<32-s,rotrSL:(t,e,s)=>t<<32-s|e>>>s,rotrBH:(t,e,s)=>t<<64-s|e>>>s-32,rotrBL:(t,e,s)=>t>>>s-32|e<<64-s,rotr32H:(t,e)=>e,rotr32L:(t,e)=>t,rotlSH:T,rotlSL:$,rotlBH:D,rotlBL:_,add:function(t,e,s,i){const r=(e>>>0)+(i>>>0);return{h:t+s+(r/2**32|0)|0,l:0|r}},add3L:(t,e,s)=>(t>>>0)+(e>>>0)+(s>>>0),add3H:(t,e,s,i)=>e+s+i+(t/2**32|0)|0,add4L:(t,e,s,i)=>(t>>>0)+(e>>>0)+(s>>>0)+(i>>>0),add4H:(t,e,s,i,r)=>e+s+i+r+(t/2**32|0)|0,add5H:(t,e,s,i,r,n)=>e+s+i+r+n+(t/2**32|0)|0,add5L:(t,e,s,i,r)=>(t>>>0)+(e>>>0)+(s>>>0)+(i>>>0)+(r>>>0)
|
||||
},[G,P]=(()=>F.split(["0x428a2f98d728ae22","0x7137449123ef65cd","0xb5c0fbcfec4d3b2f","0xe9b5dba58189dbbc","0x3956c25bf348b538","0x59f111f1b605d019","0x923f82a4af194f9b","0xab1c5ed5da6d8118","0xd807aa98a3030242","0x12835b0145706fbe","0x243185be4ee4b28c","0x550c7dc3d5ffb4e2","0x72be5d74f27b896f","0x80deb1fe3b1696b1","0x9bdc06a725c71235","0xc19bf174cf692694","0xe49b69c19ef14ad2","0xefbe4786384f25e3","0x0fc19dc68b8cd5b5","0x240ca1cc77ac9c65","0x2de92c6f592b0275","0x4a7484aa6ea6e483","0x5cb0a9dcbd41fbd4","0x76f988da831153b5","0x983e5152ee66dfab","0xa831c66d2db43210","0xb00327c898fb213f","0xbf597fc7beef0ee4","0xc6e00bf33da88fc2","0xd5a79147930aa725","0x06ca6351e003826f","0x142929670a0e6e70","0x27b70a8546d22ffc","0x2e1b21385c26c926","0x4d2c6dfc5ac42aed","0x53380d139d95b3df","0x650a73548baf63de","0x766a0abb3c77b2a8","0x81c2c92e47edaee6","0x92722c851482353b","0xa2bfe8a14cf10364","0xa81a664bbc423001","0xc24b8b70d0f89791","0xc76c51a30654be30","0xd192e819d6ef5218","0xd69906245565a910","0xf40e35855771202a","0x106aa07032bbd1b8","0x19a4c116b8d2d0c8","0x1e376c085141ab53","0x2748774cdf8eeb99","0x34b0bcb5e19b48a8","0x391c0cb3c5c95a63","0x4ed8aa4ae3418acb","0x5b9cca4f7763e373","0x682e6ff3d6b2b8a3","0x748f82ee5defb2fc","0x78a5636f43172f60","0x84c87814a1f0ab72","0x8cc702081a6439ec","0x90befffa23631e28","0xa4506cebde82bde9","0xbef9a3f7b2c67915","0xc67178f2e372532b","0xca273eceea26619c","0xd186b8c721c0c207","0xeada7dd6cde0eb1e","0xf57d4f7fee6ed178","0x06f067aa72176fba","0x0a637dc5a2c898a6","0x113f9804bef90dae","0x1b710b35131c471b","0x28db77f523047d84","0x32caab7b40c72493","0x3c9ebe0a15c9bebc","0x431d67c49c100d4c","0x4cc5d4becb3e42b6","0x597f299cfc657e2a","0x5fcb6fab3ad6faec","0x6c44198c4a475817"].map((t=>BigInt(t)))))(),j=new Uint32Array(80),M=new Uint32Array(80);class R extends w{get(){const{Ah:t,Al:e,Bh:s,Bl:i,Ch:r,Cl:n,Dh:o,Dl:h,Eh:a,El:l,Fh:c,Fl:u,Gh:d,Gl:f,Hh:b,Hl:g}=this;return[t,e,s,i,r,n,o,h,a,l,c,u,d,f,b,g]}set(t,e,s,i,r,n,o,h,a,l,c,u,d,f,b,g){this.Ah=0|t,this.Al=0|e,this.Bh=0|s,this.Bl=0|i,this.Ch=0|r,this.Cl=0|n,this.Dh=0|o,
|
||||
this.Dl=0|h,this.Eh=0|a,this.El=0|l,this.Fh=0|c,this.Fl=0|u,this.Gh=0|d,this.Gl=0|f,this.Hh=0|b,this.Hl=0|g}process(t,e){for(let s=0;s<16;s++,e+=4)j[s]=t.getUint32(e),M[s]=t.getUint32(e+=4);for(let t=16;t<80;t++){const e=0|j[t-15],s=0|M[t-15],i=F.rotrSH(e,s,1)^F.rotrSH(e,s,8)^F.shrSH(e,s,7),r=F.rotrSL(e,s,1)^F.rotrSL(e,s,8)^F.shrSL(e,s,7),n=0|j[t-2],o=0|M[t-2],h=F.rotrSH(n,o,19)^F.rotrBH(n,o,61)^F.shrSH(n,o,6),a=F.rotrSL(n,o,19)^F.rotrBL(n,o,61)^F.shrSL(n,o,6),l=F.add4L(r,a,M[t-7],M[t-16]),c=F.add4H(l,i,h,j[t-7],j[t-16]);j[t]=0|c,M[t]=0|l}let{Ah:s,Al:i,Bh:r,Bl:n,Ch:o,Cl:h,Dh:a,Dl:l,Eh:c,El:u,Fh:d,Fl:f,Gh:b,Gl:g,Hh:p,Hl:w}=this;for(let t=0;t<80;t++){const e=F.rotrSH(c,u,14)^F.rotrSH(c,u,18)^F.rotrBH(c,u,41),y=F.rotrSL(c,u,14)^F.rotrSL(c,u,18)^F.rotrBL(c,u,41),x=c&d^~c&b,A=u&f^~u&g,m=F.add5L(w,y,A,P[t],M[t]),H=F.add5H(m,p,e,x,G[t],j[t]),L=0|m,I=F.rotrSH(s,i,28)^F.rotrBH(s,i,34)^F.rotrBH(s,i,39),S=F.rotrSL(s,i,28)^F.rotrBL(s,i,34)^F.rotrBL(s,i,39),B=s&r^s&o^r&o,E=i&n^i&h^n&h;p=0|b,w=0|g,b=0|d,g=0|f,d=0|c,f=0|u,({h:c,l:u}=F.add(0|a,0|l,0|H,0|L)),a=0|o,l=0|h,o=0|r,h=0|n,r=0|s,n=0|i;const U=F.add3L(L,S,E);s=F.add3H(U,H,I,B),i=0|U}({h:s,l:i}=F.add(0|this.Ah,0|this.Al,0|s,0|i)),({h:r,l:n}=F.add(0|this.Bh,0|this.Bl,0|r,0|n)),({h:o,l:h}=F.add(0|this.Ch,0|this.Cl,0|o,0|h)),({h:a,l}=F.add(0|this.Dh,0|this.Dl,0|a,0|l)),({h:c,l:u}=F.add(0|this.Eh,0|this.El,0|c,0|u)),({h:d,l:f}=F.add(0|this.Fh,0|this.Fl,0|d,0|f)),({h:b,l:g}=F.add(0|this.Gh,0|this.Gl,0|b,0|g)),({h:p,l:w}=F.add(0|this.Hh,0|this.Hl,0|p,0|w)),this.set(s,i,r,n,o,h,a,l,c,u,d,f,b,g,p,w)}roundClean(){j.fill(0),M.fill(0)}destroy(){this.buffer.fill(0),this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)}constructor(){super(128,64,16,!1),this.Ah=1779033703,this.Al=-205731576,this.Bh=-1150833019,this.Bl=-2067093701,this.Ch=1013904242,this.Cl=-23791573,this.Dh=-1521486534,this.Dl=1595750129,this.Eh=1359893119,this.El=-1377402159,this.Fh=-1694144372,this.Fl=725511199,this.Gh=528734635,this.Gl=-79577749,this.Hh=1541459225,this.Hl=327033209}}class N extends R{constructor(){super(),
|
||||
this.Ah=-876896931,this.Al=-1056596264,this.Bh=1654270250,this.Bl=914150663,this.Ch=-1856437926,this.Cl=812702999,this.Dh=355462360,this.Dl=-150054599,this.Eh=1731405415,this.El=-4191439,this.Fh=-1900787065,this.Fl=1750603025,this.Gh=-619958771,this.Gl=1694076839,this.Hh=1203062813,this.Hl=-1090891868,this.outputLen=48}}const X=d((()=>new R)),V=d((()=>new N)),Z=[],z=[],J=[],K=BigInt(0),Q=BigInt(1),W=BigInt(2),Y=BigInt(7),q=BigInt(256),tt=BigInt(113);for(let t=0,e=Q,s=1,i=0;t<24;t++){[s,i]=[i,(2*s+3*i)%5],Z.push(2*(5*i+s)),z.push((t+1)*(t+2)/2%64);let r=K;for(let t=0;t<7;t++)e=(e<<Q^(e>>Y)*tt)%q,e&W&&(r^=Q<<(Q<<BigInt(t))-Q);J.push(r)}const[et,st]=k(J,!0),it=(t,e,s)=>s>32?D(t,e,s):T(t,e,s),rt=(t,e,s)=>s>32?_(t,e,s):$(t,e,s);class nt extends u{keccak(){a||l(this.state32),function(t,e=24){const s=new Uint32Array(10);for(let i=24-e;i<24;i++){for(let e=0;e<10;e++)s[e]=t[e]^t[e+10]^t[e+20]^t[e+30]^t[e+40];for(let e=0;e<10;e+=2){const i=(e+8)%10,r=(e+2)%10,n=s[r],o=s[r+1],h=it(n,o,1)^s[i],a=rt(n,o,1)^s[i+1];for(let s=0;s<50;s+=10)t[e+s]^=h,t[e+s+1]^=a}let e=t[2],r=t[3];for(let s=0;s<24;s++){const i=z[s],n=it(e,r,i),o=rt(e,r,i),h=Z[s];e=t[h],r=t[h+1],t[h]=n,t[h+1]=o}for(let e=0;e<50;e+=10){for(let i=0;i<10;i++)s[i]=t[e+i];for(let i=0;i<10;i++)t[e+i]^=~s[(i+2)%10]&s[(i+4)%10]}t[0]^=et[i],t[1]^=st[i]}s.fill(0)}(this.state32,this.rounds),a||l(this.state32),this.posOut=0,this.pos=0}update(t){i(this);const{blockLen:e,state:s}=this,r=(t=c(t)).length;for(let i=0;i<r;){const n=Math.min(e-this.pos,r-i);for(let e=0;e<n;e++)s[this.pos++]^=t[i++];this.pos===e&&this.keccak()}return this}finish(){if(this.finished)return;this.finished=!0;const{state:t,suffix:e,pos:s,blockLen:i}=this;t[s]^=e,128&e&&s===i-1&&this.keccak(),t[i-1]^=128,this.keccak()}writeInto(t){i(this,!1),s(t),this.finish();const e=this.state,{blockLen:r}=this;for(let s=0,i=t.length;s<i;){this.posOut>=r&&this.keccak();const n=Math.min(r-this.posOut,i-s);t.set(e.subarray(this.posOut,this.posOut+n),s),this.posOut+=n,s+=n}return t}xofInto(t){
|
||||
if(!this.enableXOF)throw new Error("XOF is not possible for this instance");return this.writeInto(t)}xof(t){return e(t),this.xofInto(new Uint8Array(t))}digestInto(t){if(r(t,this),this.finished)throw new Error("digest() was already called");return this.writeInto(t),this.destroy(),t}digest(){return this.digestInto(new Uint8Array(this.outputLen))}destroy(){this.destroyed=!0,this.state.fill(0)}_cloneInto(t){const{blockLen:e,suffix:s,outputLen:i,rounds:r,enableXOF:n}=this;return t||(t=new nt(e,s,i,n,r)),t.state32.set(this.state32),t.pos=this.pos,t.posOut=this.posOut,t.finished=this.finished,t.rounds=r,t.suffix=s,t.outputLen=i,t.enableXOF=n,t.destroyed=this.destroyed,t}constructor(t,s,i,r=!1,n=24){if(super(),this.blockLen=t,this.suffix=s,this.outputLen=i,this.enableXOF=r,this.rounds=n,this.pos=0,this.posOut=0,this.finished=!1,this.destroyed=!1,e(i),0>=this.blockLen||this.blockLen>=200)throw new Error("Sha3 supports only keccak-f1600 function");var o;this.state=new Uint8Array(200),this.state32=(o=this.state,new Uint32Array(o.buffer,o.byteOffset,Math.floor(o.byteLength/4)))}}const ot=(t,e,s)=>d((()=>new nt(e,t,s))),ht=ot(6,144,28),at=ot(6,136,32),lt=ot(6,104,48),ct=ot(6,72,64),ut=(()=>{if("object"==typeof globalThis)return globalThis;Object.defineProperty(Object.prototype,"__GLOBALTHIS__",{get(){return this},configurable:!0});try{if("undefined"!=typeof __GLOBALTHIS__)return __GLOBALTHIS__}finally{delete Object.prototype.__GLOBALTHIS__}return"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:void 0})(),dt={SHA1:m,SHA224:U,SHA256:E,SHA384:V,SHA512:X,"SHA3-224":ht,"SHA3-256":at,"SHA3-384":lt,"SHA3-512":ct},ft=t=>{switch(!0){case/^(?:SHA-?1|SSL3-SHA1)$/i.test(t):return"SHA1";case/^SHA(?:2?-)?224$/i.test(t):return"SHA224";case/^SHA(?:2?-)?256$/i.test(t):return"SHA256";case/^SHA(?:2?-)?384$/i.test(t):return"SHA384";case/^SHA(?:2?-)?512$/i.test(t):return"SHA512";case/^SHA3-224$/i.test(t):return"SHA3-224";case/^SHA3-256$/i.test(t):return"SHA3-256";case/^SHA3-384$/i.test(t):
|
||||
return"SHA3-384";case/^SHA3-512$/i.test(t):return"SHA3-512";default:throw new TypeError(`Unknown hash algorithm: ${t}`)}},bt="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",gt=t=>{let e=(t=t.replace(/ /g,"")).length;for(;"="===t[e-1];)--e;t=(e<t.length?t.substring(0,e):t).toUpperCase();const s=new ArrayBuffer(5*t.length/8|0),i=new Uint8Array(s);let r=0,n=0,o=0;for(let e=0;e<t.length;e++){const s=bt.indexOf(t[e]);if(-1===s)throw new TypeError(`Invalid character found: ${t[e]}`);n=n<<5|s,r+=5,r>=8&&(r-=8,i[o++]=n>>>r)}return i},pt=t=>{let e=0,s=0,i="";for(let r=0;r<t.length;r++)for(s=s<<8|t[r],e+=8;e>=5;)i+=bt[s>>>e-5&31],e-=5;return e>0&&(i+=bt[s<<5-e&31]),i},wt=t=>{t=t.replace(/ /g,"");const e=new ArrayBuffer(t.length/2),s=new Uint8Array(e);for(let e=0;e<t.length;e+=2)s[e/2]=parseInt(t.substring(e,e+2),16);return s},yt=t=>{let e="";for(let s=0;s<t.length;s++){const i=t[s].toString(16);1===i.length&&(e+="0"),e+=i}return e.toUpperCase()},xt=t=>{const e=new ArrayBuffer(t.length),s=new Uint8Array(e);for(let e=0;e<t.length;e++)s[e]=255&t.charCodeAt(e);return s},At=t=>{let e="";for(let s=0;s<t.length;s++)e+=String.fromCharCode(t[s]);return e},mt=ut.TextEncoder?new ut.TextEncoder:null,Ht=ut.TextDecoder?new ut.TextDecoder:null,Lt=t=>{if(!mt)throw new Error("Encoding API not available");return mt.encode(t)},It=t=>{if(!Ht)throw new Error("Encoding API not available");return Ht.decode(t)};class St{static fromLatin1(t){return new St({buffer:xt(t).buffer})}static fromUTF8(t){return new St({buffer:Lt(t).buffer})}static fromBase32(t){return new St({buffer:gt(t).buffer})}static fromHex(t){return new St({buffer:wt(t).buffer})}get buffer(){return this.bytes.buffer}get latin1(){return Object.defineProperty(this,"latin1",{enumerable:!0,writable:!1,configurable:!1,value:At(this.bytes)}),this.latin1}get utf8(){return Object.defineProperty(this,"utf8",{enumerable:!0,writable:!1,configurable:!1,value:It(this.bytes)}),this.utf8}get base32(){return Object.defineProperty(this,"base32",{enumerable:!0,writable:!1,configurable:!1,value:pt(this.bytes)}),
|
||||
this.base32}get hex(){return Object.defineProperty(this,"hex",{enumerable:!0,writable:!1,configurable:!1,value:yt(this.bytes)}),this.hex}constructor({buffer:t,size:e=20}={}){this.bytes=void 0===t?(t=>{if(ut.crypto?.getRandomValues)return ut.crypto.getRandomValues(new Uint8Array(t));throw new Error("Cryptography API not available")})(e):new Uint8Array(t),Object.defineProperty(this,"bytes",{enumerable:!0,writable:!1,configurable:!1,value:this.bytes})}}class Bt{static get defaults(){return{issuer:"",label:"OTPAuth",issuerInLabel:!0,algorithm:"SHA1",digits:6,counter:0,window:1}}static generate({secret:t,algorithm:e=Bt.defaults.algorithm,digits:s=Bt.defaults.digits,counter:i=Bt.defaults.counter}){const r=((t,e,s)=>{if(b){const i=dt[t]??dt[ft(t)];return b(i,e,s)}throw new Error("Missing HMAC function")})(e,t.bytes,(t=>{const e=new ArrayBuffer(8),s=new Uint8Array(e);let i=t;for(let t=7;t>=0&&0!==i;t--)s[t]=255&i,i-=s[t],i/=256;return s})(i)),n=15&r[r.byteLength-1];return(((127&r[n])<<24|(255&r[n+1])<<16|(255&r[n+2])<<8|255&r[n+3])%10**s).toString().padStart(s,"0")}generate({counter:t=this.counter++}={}){return Bt.generate({secret:this.secret,algorithm:this.algorithm,digits:this.digits,counter:t})}static validate({token:t,secret:e,algorithm:s,digits:i=Bt.defaults.digits,counter:r=Bt.defaults.counter,window:n=Bt.defaults.window}){if(t.length!==i)return null;let o=null;const h=n=>{const h=Bt.generate({secret:e,algorithm:s,digits:i,counter:n});((t,e)=>{{if(t.length!==e.length)throw new TypeError("Input strings must have the same length");let s=-1,i=0;for(;++s<t.length;)i|=t.charCodeAt(s)^e.charCodeAt(s);return 0===i}})(t,h)&&(o=n-r)};h(r);for(let t=1;t<=n&&null===o&&(h(r-t),null===o)&&(h(r+t),null===o);++t);return o}validate({token:t,counter:e=this.counter,window:s}){return Bt.validate({token:t,secret:this.secret,algorithm:this.algorithm,digits:this.digits,counter:e,window:s})}toString(){const t=encodeURIComponent
|
||||
;return"otpauth://hotp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`counter=${t(this.counter)}`}constructor({issuer:t=Bt.defaults.issuer,label:e=Bt.defaults.label,issuerInLabel:s=Bt.defaults.issuerInLabel,secret:i=new St,algorithm:r=Bt.defaults.algorithm,digits:n=Bt.defaults.digits,counter:o=Bt.defaults.counter}={}){this.issuer=t,this.label=e,this.issuerInLabel=s,this.secret="string"==typeof i?St.fromBase32(i):i,this.algorithm=ft(r),this.digits=n,this.counter=o}}class Et{static get defaults(){return{issuer:"",label:"OTPAuth",issuerInLabel:!0,algorithm:"SHA1",digits:6,period:30,window:1}}static counter({period:t=Et.defaults.period,timestamp:e=Date.now()}={}){return Math.floor(e/1e3/t)}counter({timestamp:t=Date.now()}={}){return Et.counter({period:this.period,timestamp:t})}static remaining({period:t=Et.defaults.period,timestamp:e=Date.now()}={}){return 1e3*t-e%(1e3*t)}remaining({timestamp:t=Date.now()}={}){return Et.remaining({period:this.period,timestamp:t})}static generate({secret:t,algorithm:e,digits:s,period:i=Et.defaults.period,timestamp:r=Date.now()}){return Bt.generate({secret:t,algorithm:e,digits:s,counter:Et.counter({period:i,timestamp:r})})}generate({timestamp:t=Date.now()}={}){return Et.generate({secret:this.secret,algorithm:this.algorithm,digits:this.digits,period:this.period,timestamp:t})}static validate({token:t,secret:e,algorithm:s,digits:i,period:r=Et.defaults.period,timestamp:n=Date.now(),window:o}){return Bt.validate({token:t,secret:e,algorithm:s,digits:i,counter:Et.counter({period:r,timestamp:n}),window:o})}validate({token:t,timestamp:e,window:s}){return Et.validate({token:t,secret:this.secret,algorithm:this.algorithm,digits:this.digits,period:this.period,timestamp:e,window:s})}toString(){const t=encodeURIComponent
|
||||
;return"otpauth://totp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`period=${t(this.period)}`}constructor({issuer:t=Et.defaults.issuer,label:e=Et.defaults.label,issuerInLabel:s=Et.defaults.issuerInLabel,secret:i=new St,algorithm:r=Et.defaults.algorithm,digits:n=Et.defaults.digits,period:o=Et.defaults.period}={}){this.issuer=t,this.label=e,this.issuerInLabel=s,this.secret="string"==typeof i?St.fromBase32(i):i,this.algorithm=ft(r),this.digits=n,this.period=o}}const Ut=/^otpauth:\/\/([ht]otp)\/(.+)\?([A-Z0-9.~_-]+=[^?&]*(?:&[A-Z0-9.~_-]+=[^?&]*)*)$/i,Ct=/^[2-7A-Z]+=*$/i,Ot=/^SHA(?:1|224|256|384|512|3-224|3-256|3-384|3-512)$/i,vt=/^[+-]?\d+$/,kt=/^\+?[1-9]\d*$/;t.HOTP=Bt,t.Secret=St,t.TOTP=Et,t.URI=class{static parse(t){let e;try{e=t.match(Ut)}catch(t){}if(!Array.isArray(e))throw new URIError("Invalid URI format");const s=e[1].toLowerCase(),i=e[2].split(/(?::|%3A) *(.+)/i,2).map(decodeURIComponent),r=e[3].split("&").reduce(((t,e)=>{const s=e.split(/=(.*)/,2).map(decodeURIComponent),i=s[0].toLowerCase(),r=s[1],n=t;return n[i]=r,n}),{});let n;const o={};if("hotp"===s){if(n=Bt,void 0===r.counter||!vt.test(r.counter))throw new TypeError("Missing or invalid 'counter' parameter");o.counter=parseInt(r.counter,10)}else{if("totp"!==s)throw new TypeError("Unknown OTP type");if(n=Et,void 0!==r.period){if(!kt.test(r.period))throw new TypeError("Invalid 'period' parameter");o.period=parseInt(r.period,10)}}if(void 0!==r.issuer&&(o.issuer=r.issuer),2===i.length?(o.label=i[1],void 0===o.issuer||""===o.issuer?o.issuer=i[0]:""===i[0]&&(o.issuerInLabel=!1)):(o.label=i[0],void 0!==o.issuer&&""!==o.issuer&&(o.issuerInLabel=!1)),void 0===r.secret||!Ct.test(r.secret))throw new TypeError("Missing or invalid 'secret' parameter");if(o.secret=r.secret,void 0!==r.algorithm){
|
||||
if(!Ot.test(r.algorithm))throw new TypeError("Invalid 'algorithm' parameter");o.algorithm=r.algorithm}if(void 0!==r.digits){if(!kt.test(r.digits))throw new TypeError("Invalid 'digits' parameter");o.digits=parseInt(r.digits,10)}return new n(o)}static stringify(t){if(t instanceof Bt||t instanceof Et)return t.toString();throw new TypeError("Invalid 'HOTP/TOTP' object")}},t.version="9.4.0"}));
|
||||
//# sourceMappingURL=otpauth.umd.min.js.map
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).OTPAuth={})}(this,function(t){"use strict";function e(t){if(!Number.isSafeInteger(t)||t<0)throw new Error("positive integer expected, got "+t)}function s(t,...e){if(!((s=t)instanceof Uint8Array||ArrayBuffer.isView(s)&&"Uint8Array"===s.constructor.name))throw new Error("Uint8Array expected");var s;if(e.length>0&&!e.includes(t.length))throw new Error("Uint8Array expected of length "+e+", got length="+t.length)}function i(t,e=!0){if(t.destroyed)throw new Error("Hash instance has been destroyed");if(e&&t.finished)throw new Error("Hash#digest() has already been called")}function r(t,e){s(t);const i=e.outputLen;if(t.length<i)throw new Error("digestInto() expects output buffer of length at least "+i)}function n(...t){for(let e=0;e<t.length;e++)t[e].fill(0)}function o(t){return new DataView(t.buffer,t.byteOffset,t.byteLength)}function h(t,e){return t<<32-e|t>>>e}function a(t,e){return t<<e|t>>>32-e>>>0}function c(t){return t<<24&4278190080|t<<8&16711680|t>>>8&65280|t>>>24&255}const l=(()=>68===new Uint8Array(new Uint32Array([287454020]).buffer)[0])()?t=>t:function(t){for(let e=0;e<t.length;e++)t[e]=c(t[e]);return t};function u(t){return"string"==typeof t&&(t=function(t){if("string"!=typeof t)throw new Error("string expected");return new Uint8Array((new TextEncoder).encode(t))}(t)),s(t),t}class f{}function d(t){const e=e=>t().update(u(e)).digest(),s=t();return e.outputLen=s.outputLen,e.blockLen=s.blockLen,e.create=()=>t(),e}class b extends f{update(t){return i(this),this.iHash.update(t),this}digestInto(t){i(this),s(t,this.outputLen),this.finished=!0,this.iHash.digestInto(t),this.oHash.update(t),this.oHash.digestInto(t),this.destroy()}digest(){const t=new Uint8Array(this.oHash.outputLen);return this.digestInto(t),t}_cloneInto(t){t||(t=Object.create(Object.getPrototypeOf(this),{}))
|
||||
;const{oHash:e,iHash:s,finished:i,destroyed:r,blockLen:n,outputLen:o}=this;return t.finished=i,t.destroyed=r,t.blockLen=n,t.outputLen=o,t.oHash=e._cloneInto(t.oHash),t.iHash=s._cloneInto(t.iHash),t}clone(){return this._cloneInto()}destroy(){this.destroyed=!0,this.oHash.destroy(),this.iHash.destroy()}constructor(t,s){super(),this.finished=!1,this.destroyed=!1,function(t){if("function"!=typeof t||"function"!=typeof t.create)throw new Error("Hash should be wrapped by utils.createHasher");e(t.outputLen),e(t.blockLen)}(t);const i=u(s);if(this.iHash=t.create(),"function"!=typeof this.iHash.update)throw new Error("Expected instance of class which extends utils.Hash");this.blockLen=this.iHash.blockLen,this.outputLen=this.iHash.outputLen;const r=this.blockLen,o=new Uint8Array(r);o.set(i.length>r?t.create().update(i).digest():i);for(let t=0;t<o.length;t++)o[t]^=54;this.iHash.update(o),this.oHash=t.create();for(let t=0;t<o.length;t++)o[t]^=106;this.oHash.update(o),n(o)}}const g=(t,e,s)=>new b(t,e).update(s).digest();function p(t,e,s){return t&e^~t&s}function w(t,e,s){return t&e^t&s^e&s}g.create=(t,e)=>new b(t,e);class y extends f{update(t){i(this),s(t=u(t));const{view:e,buffer:r,blockLen:n}=this,h=t.length;for(let s=0;s<h;){const i=Math.min(n-this.pos,h-s);if(i===n){const e=o(t);for(;n<=h-s;s+=n)this.process(e,s);continue}r.set(t.subarray(s,s+i),this.pos),this.pos+=i,s+=i,this.pos===n&&(this.process(e,0),this.pos=0)}return this.length+=t.length,this.roundClean(),this}digestInto(t){i(this),r(t,this),this.finished=!0;const{buffer:e,view:s,blockLen:h,isLE:a}=this;let{pos:c}=this;e[c++]=128,n(this.buffer.subarray(c)),this.padOffset>h-c&&(this.process(s,0),c=0);for(let t=c;t<h;t++)e[t]=0;!function(t,e,s,i){if("function"==typeof t.setBigUint64)return t.setBigUint64(e,s,i);const r=BigInt(32),n=BigInt(4294967295),o=Number(s>>r&n),h=Number(s&n),a=i?4:0,c=i?0:4;t.setUint32(e+a,o,i),t.setUint32(e+c,h,i)}(s,h-8,BigInt(8*this.length),a),this.process(s,0);const l=o(t),u=this.outputLen
|
||||
;if(u%4)throw new Error("_sha2: outputLen should be aligned to 32bit");const f=u/4,d=this.get();if(f>d.length)throw new Error("_sha2: outputLen bigger than state");for(let t=0;t<f;t++)l.setUint32(4*t,d[t],a)}digest(){const{buffer:t,outputLen:e}=this;this.digestInto(t);const s=t.slice(0,e);return this.destroy(),s}_cloneInto(t){t||(t=new this.constructor),t.set(...this.get());const{blockLen:e,buffer:s,length:i,finished:r,destroyed:n,pos:o}=this;return t.destroyed=n,t.finished=r,t.length=i,t.pos=o,i%e&&t.buffer.set(s),t}clone(){return this._cloneInto()}constructor(t,e,s,i){super(),this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.blockLen=t,this.outputLen=e,this.padOffset=s,this.isLE=i,this.buffer=new Uint8Array(t),this.view=o(this.buffer)}}const x=Uint32Array.from([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),m=Uint32Array.from([3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428]),A=Uint32Array.from([3418070365,3238371032,1654270250,914150663,2438529370,812702999,355462360,4144912697,1731405415,4290775857,2394180231,1750603025,3675008525,1694076839,1203062813,3204075428]),H=Uint32Array.from([1779033703,4089235720,3144134277,2227873595,1013904242,4271175723,2773480762,1595750129,1359893119,2917565137,2600822924,725511199,528734635,4215389547,1541459225,327033209]),I=Uint32Array.from([1732584193,4023233417,2562383102,271733878,3285377520]),L=new Uint32Array(80);class E extends y{get(){const{A:t,B:e,C:s,D:i,E:r}=this;return[t,e,s,i,r]}set(t,e,s,i,r){this.A=0|t,this.B=0|e,this.C=0|s,this.D=0|i,this.E=0|r}process(t,e){for(let s=0;s<16;s++,e+=4)L[s]=t.getUint32(e,!1);for(let t=16;t<80;t++)L[t]=a(L[t-3]^L[t-8]^L[t-14]^L[t-16],1);let{A:s,B:i,C:r,D:n,E:o}=this;for(let t=0;t<80;t++){let e,h;t<20?(e=p(i,r,n),h=1518500249):t<40?(e=i^r^n,h=1859775393):t<60?(e=w(i,r,n),h=2400959708):(e=i^r^n,h=3395469782);const c=a(s,5)+e+o+h+L[t]|0;o=n,n=r,r=a(i,30),i=s,s=c}s=s+this.A|0,i=i+this.B|0,r=r+this.C|0,n=n+this.D|0,o=o+this.E|0,
|
||||
this.set(s,i,r,n,o)}roundClean(){n(L)}destroy(){this.set(0,0,0,0,0),n(this.buffer)}constructor(){super(64,20,8,!1),this.A=0|I[0],this.B=0|I[1],this.C=0|I[2],this.D=0|I[3],this.E=0|I[4]}}const U=d(()=>new E),B=BigInt(2**32-1),S=BigInt(32);function O(t,e=!1){return e?{h:Number(t&B),l:Number(t>>S&B)}:{h:0|Number(t>>S&B),l:0|Number(t&B)}}function C(t,e=!1){const s=t.length;let i=new Uint32Array(s),r=new Uint32Array(s);for(let n=0;n<s;n++){const{h:s,l:o}=O(t[n],e);[i[n],r[n]]=[s,o]}return[i,r]}const v=(t,e,s)=>t>>>s,k=(t,e,s)=>t<<32-s|e>>>s,$=(t,e,s)=>t>>>s|e<<32-s,T=(t,e,s)=>t<<32-s|e>>>s,D=(t,e,s)=>t<<64-s|e>>>s-32,_=(t,e,s)=>t>>>s-32|e<<64-s;function F(t,e,s,i){const r=(e>>>0)+(i>>>0);return{h:t+s+(r/2**32|0)|0,l:0|r}}const G=(t,e,s)=>(t>>>0)+(e>>>0)+(s>>>0),P=(t,e,s,i)=>e+s+i+(t/2**32|0)|0,j=(t,e,s,i)=>(t>>>0)+(e>>>0)+(s>>>0)+(i>>>0),M=(t,e,s,i,r)=>e+s+i+r+(t/2**32|0)|0,R=(t,e,s,i,r)=>(t>>>0)+(e>>>0)+(s>>>0)+(i>>>0)+(r>>>0),N=(t,e,s,i,r,n)=>e+s+i+r+n+(t/2**32|0)|0,X=Uint32Array.from([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),V=new Uint32Array(64);class Z extends y{get(){const{A:t,B:e,C:s,D:i,E:r,F:n,G:o,H:h}=this;return[t,e,s,i,r,n,o,h]}set(t,e,s,i,r,n,o,h){this.A=0|t,this.B=0|e,this.C=0|s,this.D=0|i,this.E=0|r,this.F=0|n,this.G=0|o,this.H=0|h}process(t,e){for(let s=0;s<16;s++,e+=4)V[s]=t.getUint32(e,!1);for(let t=16;t<64;t++){
|
||||
const e=V[t-15],s=V[t-2],i=h(e,7)^h(e,18)^e>>>3,r=h(s,17)^h(s,19)^s>>>10;V[t]=r+V[t-7]+i+V[t-16]|0}let{A:s,B:i,C:r,D:n,E:o,F:a,G:c,H:l}=this;for(let t=0;t<64;t++){const e=l+(h(o,6)^h(o,11)^h(o,25))+p(o,a,c)+X[t]+V[t]|0,u=(h(s,2)^h(s,13)^h(s,22))+w(s,i,r)|0;l=c,c=a,a=o,o=n+e|0,n=r,r=i,i=s,s=e+u|0}s=s+this.A|0,i=i+this.B|0,r=r+this.C|0,n=n+this.D|0,o=o+this.E|0,a=a+this.F|0,c=c+this.G|0,l=l+this.H|0,this.set(s,i,r,n,o,a,c,l)}roundClean(){n(V)}destroy(){this.set(0,0,0,0,0,0,0,0),n(this.buffer)}constructor(t=32){super(64,t,8,!1),this.A=0|x[0],this.B=0|x[1],this.C=0|x[2],this.D=0|x[3],this.E=0|x[4],this.F=0|x[5],this.G=0|x[6],this.H=0|x[7]}}class z extends Z{constructor(){super(28),this.A=0|m[0],this.B=0|m[1],this.C=0|m[2],this.D=0|m[3],this.E=0|m[4],this.F=0|m[5],this.G=0|m[6],this.H=0|m[7]}}
|
||||
const J=(()=>C(["0x428a2f98d728ae22","0x7137449123ef65cd","0xb5c0fbcfec4d3b2f","0xe9b5dba58189dbbc","0x3956c25bf348b538","0x59f111f1b605d019","0x923f82a4af194f9b","0xab1c5ed5da6d8118","0xd807aa98a3030242","0x12835b0145706fbe","0x243185be4ee4b28c","0x550c7dc3d5ffb4e2","0x72be5d74f27b896f","0x80deb1fe3b1696b1","0x9bdc06a725c71235","0xc19bf174cf692694","0xe49b69c19ef14ad2","0xefbe4786384f25e3","0x0fc19dc68b8cd5b5","0x240ca1cc77ac9c65","0x2de92c6f592b0275","0x4a7484aa6ea6e483","0x5cb0a9dcbd41fbd4","0x76f988da831153b5","0x983e5152ee66dfab","0xa831c66d2db43210","0xb00327c898fb213f","0xbf597fc7beef0ee4","0xc6e00bf33da88fc2","0xd5a79147930aa725","0x06ca6351e003826f","0x142929670a0e6e70","0x27b70a8546d22ffc","0x2e1b21385c26c926","0x4d2c6dfc5ac42aed","0x53380d139d95b3df","0x650a73548baf63de","0x766a0abb3c77b2a8","0x81c2c92e47edaee6","0x92722c851482353b","0xa2bfe8a14cf10364","0xa81a664bbc423001","0xc24b8b70d0f89791","0xc76c51a30654be30","0xd192e819d6ef5218","0xd69906245565a910","0xf40e35855771202a","0x106aa07032bbd1b8","0x19a4c116b8d2d0c8","0x1e376c085141ab53","0x2748774cdf8eeb99","0x34b0bcb5e19b48a8","0x391c0cb3c5c95a63","0x4ed8aa4ae3418acb","0x5b9cca4f7763e373","0x682e6ff3d6b2b8a3","0x748f82ee5defb2fc","0x78a5636f43172f60","0x84c87814a1f0ab72","0x8cc702081a6439ec","0x90befffa23631e28","0xa4506cebde82bde9","0xbef9a3f7b2c67915","0xc67178f2e372532b","0xca273eceea26619c","0xd186b8c721c0c207","0xeada7dd6cde0eb1e","0xf57d4f7fee6ed178","0x06f067aa72176fba","0x0a637dc5a2c898a6","0x113f9804bef90dae","0x1b710b35131c471b","0x28db77f523047d84","0x32caab7b40c72493","0x3c9ebe0a15c9bebc","0x431d67c49c100d4c","0x4cc5d4becb3e42b6","0x597f299cfc657e2a","0x5fcb6fab3ad6faec","0x6c44198c4a475817"].map(t=>BigInt(t))))(),K=(()=>J[0])(),Q=(()=>J[1])(),W=new Uint32Array(80),Y=new Uint32Array(80);class q extends y{get(){const{Ah:t,Al:e,Bh:s,Bl:i,Ch:r,Cl:n,Dh:o,Dl:h,Eh:a,El:c,Fh:l,Fl:u,Gh:f,Gl:d,Hh:b,Hl:g}=this;return[t,e,s,i,r,n,o,h,a,c,l,u,f,d,b,g]}set(t,e,s,i,r,n,o,h,a,c,l,u,f,d,b,g){this.Ah=0|t,this.Al=0|e,this.Bh=0|s,this.Bl=0|i,this.Ch=0|r,
|
||||
this.Cl=0|n,this.Dh=0|o,this.Dl=0|h,this.Eh=0|a,this.El=0|c,this.Fh=0|l,this.Fl=0|u,this.Gh=0|f,this.Gl=0|d,this.Hh=0|b,this.Hl=0|g}process(t,e){for(let s=0;s<16;s++,e+=4)W[s]=t.getUint32(e),Y[s]=t.getUint32(e+=4);for(let t=16;t<80;t++){const e=0|W[t-15],s=0|Y[t-15],i=$(e,s,1)^$(e,s,8)^v(e,0,7),r=T(e,s,1)^T(e,s,8)^k(e,s,7),n=0|W[t-2],o=0|Y[t-2],h=$(n,o,19)^D(n,o,61)^v(n,0,6),a=T(n,o,19)^_(n,o,61)^k(n,o,6),c=j(r,a,Y[t-7],Y[t-16]),l=M(c,i,h,W[t-7],W[t-16]);W[t]=0|l,Y[t]=0|c}let{Ah:s,Al:i,Bh:r,Bl:n,Ch:o,Cl:h,Dh:a,Dl:c,Eh:l,El:u,Fh:f,Fl:d,Gh:b,Gl:g,Hh:p,Hl:w}=this;for(let t=0;t<80;t++){const e=$(l,u,14)^$(l,u,18)^D(l,u,41),y=T(l,u,14)^T(l,u,18)^_(l,u,41),x=l&f^~l&b,m=R(w,y,u&d^~u&g,Q[t],Y[t]),A=N(m,p,e,x,K[t],W[t]),H=0|m,I=$(s,i,28)^D(s,i,34)^D(s,i,39),L=T(s,i,28)^_(s,i,34)^_(s,i,39),E=s&r^s&o^r&o,U=i&n^i&h^n&h;p=0|b,w=0|g,b=0|f,g=0|d,f=0|l,d=0|u,({h:l,l:u}=F(0|a,0|c,0|A,0|H)),a=0|o,c=0|h,o=0|r,h=0|n,r=0|s,n=0|i;const B=G(H,L,U);s=P(B,A,I,E),i=0|B}({h:s,l:i}=F(0|this.Ah,0|this.Al,0|s,0|i)),({h:r,l:n}=F(0|this.Bh,0|this.Bl,0|r,0|n)),({h:o,l:h}=F(0|this.Ch,0|this.Cl,0|o,0|h)),({h:a,l:c}=F(0|this.Dh,0|this.Dl,0|a,0|c)),({h:l,l:u}=F(0|this.Eh,0|this.El,0|l,0|u)),({h:f,l:d}=F(0|this.Fh,0|this.Fl,0|f,0|d)),({h:b,l:g}=F(0|this.Gh,0|this.Gl,0|b,0|g)),({h:p,l:w}=F(0|this.Hh,0|this.Hl,0|p,0|w)),this.set(s,i,r,n,o,h,a,c,l,u,f,d,b,g,p,w)}roundClean(){n(W,Y)}destroy(){n(this.buffer),this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)}constructor(t=64){super(128,t,16,!1),this.Ah=0|H[0],this.Al=0|H[1],this.Bh=0|H[2],this.Bl=0|H[3],this.Ch=0|H[4],this.Cl=0|H[5],this.Dh=0|H[6],this.Dl=0|H[7],this.Eh=0|H[8],this.El=0|H[9],this.Fh=0|H[10],this.Fl=0|H[11],this.Gh=0|H[12],this.Gl=0|H[13],this.Hh=0|H[14],this.Hl=0|H[15]}}class tt extends q{constructor(){super(48),this.Ah=0|A[0],this.Al=0|A[1],this.Bh=0|A[2],this.Bl=0|A[3],this.Ch=0|A[4],this.Cl=0|A[5],this.Dh=0|A[6],this.Dl=0|A[7],this.Eh=0|A[8],this.El=0|A[9],this.Fh=0|A[10],this.Fl=0|A[11],this.Gh=0|A[12],this.Gl=0|A[13],this.Hh=0|A[14],this.Hl=0|A[15]}}
|
||||
const et=d(()=>new Z),st=d(()=>new z),it=d(()=>new q),rt=d(()=>new tt),nt=BigInt(0),ot=BigInt(1),ht=BigInt(2),at=BigInt(7),ct=BigInt(256),lt=BigInt(113),ut=[],ft=[],dt=[];for(let t=0,e=ot,s=1,i=0;t<24;t++){[s,i]=[i,(2*s+3*i)%5],ut.push(2*(5*i+s)),ft.push((t+1)*(t+2)/2%64);let r=nt;for(let t=0;t<7;t++)e=(e<<ot^(e>>at)*lt)%ct,e&ht&&(r^=ot<<(ot<<BigInt(t))-ot);dt.push(r)}const bt=C(dt,!0),gt=bt[0],pt=bt[1],wt=(t,e,s)=>s>32?((t,e,s)=>e<<s-32|t>>>64-s)(t,e,s):((t,e,s)=>t<<s|e>>>32-s)(t,e,s),yt=(t,e,s)=>s>32?((t,e,s)=>t<<s-32|e>>>64-s)(t,e,s):((t,e,s)=>e<<s|t>>>32-s)(t,e,s);class xt extends f{clone(){return this._cloneInto()}keccak(){l(this.state32),function(t,e=24){const s=new Uint32Array(10);for(let i=24-e;i<24;i++){for(let e=0;e<10;e++)s[e]=t[e]^t[e+10]^t[e+20]^t[e+30]^t[e+40];for(let e=0;e<10;e+=2){const i=(e+8)%10,r=(e+2)%10,n=s[r],o=s[r+1],h=wt(n,o,1)^s[i],a=yt(n,o,1)^s[i+1];for(let s=0;s<50;s+=10)t[e+s]^=h,t[e+s+1]^=a}let e=t[2],r=t[3];for(let s=0;s<24;s++){const i=ft[s],n=wt(e,r,i),o=yt(e,r,i),h=ut[s];e=t[h],r=t[h+1],t[h]=n,t[h+1]=o}for(let e=0;e<50;e+=10){for(let i=0;i<10;i++)s[i]=t[e+i];for(let i=0;i<10;i++)t[e+i]^=~s[(i+2)%10]&s[(i+4)%10]}t[0]^=gt[i],t[1]^=pt[i]}n(s)}(this.state32,this.rounds),l(this.state32),this.posOut=0,this.pos=0}update(t){i(this),s(t=u(t));const{blockLen:e,state:r}=this,n=t.length;for(let s=0;s<n;){const i=Math.min(e-this.pos,n-s);for(let e=0;e<i;e++)r[this.pos++]^=t[s++];this.pos===e&&this.keccak()}return this}finish(){if(this.finished)return;this.finished=!0;const{state:t,suffix:e,pos:s,blockLen:i}=this;t[s]^=e,128&e&&s===i-1&&this.keccak(),t[i-1]^=128,this.keccak()}writeInto(t){i(this,!1),s(t),this.finish();const e=this.state,{blockLen:r}=this;for(let s=0,i=t.length;s<i;){this.posOut>=r&&this.keccak();const n=Math.min(r-this.posOut,i-s);t.set(e.subarray(this.posOut,this.posOut+n),s),this.posOut+=n,s+=n}return t}xofInto(t){if(!this.enableXOF)throw new Error("XOF is not possible for this instance");return this.writeInto(t)}xof(t){return e(t),this.xofInto(new Uint8Array(t))}
|
||||
digestInto(t){if(r(t,this),this.finished)throw new Error("digest() was already called");return this.writeInto(t),this.destroy(),t}digest(){return this.digestInto(new Uint8Array(this.outputLen))}destroy(){this.destroyed=!0,n(this.state)}_cloneInto(t){const{blockLen:e,suffix:s,outputLen:i,rounds:r,enableXOF:n}=this;return t||(t=new xt(e,s,i,n,r)),t.state32.set(this.state32),t.pos=this.pos,t.posOut=this.posOut,t.finished=this.finished,t.rounds=r,t.suffix=s,t.outputLen=i,t.enableXOF=n,t.destroyed=this.destroyed,t}constructor(t,s,i,r=!1,n=24){if(super(),this.pos=0,this.posOut=0,this.finished=!1,this.destroyed=!1,this.enableXOF=!1,this.blockLen=t,this.suffix=s,this.outputLen=i,this.enableXOF=r,this.rounds=n,e(i),!(0<t&&t<200))throw new Error("only keccak-f1600 function is supported");var o;this.state=new Uint8Array(200),this.state32=(o=this.state,new Uint32Array(o.buffer,o.byteOffset,Math.floor(o.byteLength/4)))}}const mt=(t,e,s)=>d(()=>new xt(e,t,s)),At=(()=>mt(6,144,28))(),Ht=(()=>mt(6,136,32))(),It=(()=>mt(6,104,48))(),Lt=(()=>mt(6,72,64))(),Et=(()=>{if("object"==typeof globalThis)return globalThis;Object.defineProperty(Object.prototype,"__GLOBALTHIS__",{get(){return this},configurable:!0});try{if("undefined"!=typeof __GLOBALTHIS__)return __GLOBALTHIS__}finally{delete Object.prototype.__GLOBALTHIS__}return"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:void 0})(),Ut={SHA1:U,SHA224:st,SHA256:et,SHA384:rt,SHA512:it,"SHA3-224":At,"SHA3-256":Ht,"SHA3-384":It,"SHA3-512":Lt},Bt=t=>{switch(!0){case/^(?:SHA-?1|SSL3-SHA1)$/i.test(t):return"SHA1";case/^SHA(?:2?-)?224$/i.test(t):return"SHA224";case/^SHA(?:2?-)?256$/i.test(t):return"SHA256";case/^SHA(?:2?-)?384$/i.test(t):return"SHA384";case/^SHA(?:2?-)?512$/i.test(t):return"SHA512";case/^SHA3-224$/i.test(t):return"SHA3-224";case/^SHA3-256$/i.test(t):return"SHA3-256";case/^SHA3-384$/i.test(t):return"SHA3-384";case/^SHA3-512$/i.test(t):return"SHA3-512";default:throw new TypeError(`Unknown hash algorithm: ${t}`)}
|
||||
},St="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",Ot=t=>{let e=(t=t.replace(/ /g,"")).length;for(;"="===t[e-1];)--e;t=(e<t.length?t.substring(0,e):t).toUpperCase();const s=new ArrayBuffer(5*t.length/8|0),i=new Uint8Array(s);let r=0,n=0,o=0;for(let e=0;e<t.length;e++){const s=St.indexOf(t[e]);if(-1===s)throw new TypeError(`Invalid character found: ${t[e]}`);n=n<<5|s,r+=5,r>=8&&(r-=8,i[o++]=n>>>r)}return i},Ct=t=>{let e=0,s=0,i="";for(let r=0;r<t.length;r++)for(s=s<<8|t[r],e+=8;e>=5;)i+=St[s>>>e-5&31],e-=5;return e>0&&(i+=St[s<<5-e&31]),i},vt=t=>{t=t.replace(/ /g,"");const e=new ArrayBuffer(t.length/2),s=new Uint8Array(e);for(let e=0;e<t.length;e+=2)s[e/2]=parseInt(t.substring(e,e+2),16);return s},kt=t=>{let e="";for(let s=0;s<t.length;s++){const i=t[s].toString(16);1===i.length&&(e+="0"),e+=i}return e.toUpperCase()},$t=t=>{const e=new ArrayBuffer(t.length),s=new Uint8Array(e);for(let e=0;e<t.length;e++)s[e]=255&t.charCodeAt(e);return s},Tt=t=>{let e="";for(let s=0;s<t.length;s++)e+=String.fromCharCode(t[s]);return e},Dt=Et.TextEncoder?new Et.TextEncoder:null,_t=Et.TextDecoder?new Et.TextDecoder:null,Ft=t=>{if(!Dt)throw new Error("Encoding API not available");return Dt.encode(t)},Gt=t=>{if(!_t)throw new Error("Encoding API not available");return _t.decode(t)};class Pt{static fromLatin1(t){return new Pt({buffer:$t(t).buffer})}static fromUTF8(t){return new Pt({buffer:Ft(t).buffer})}static fromBase32(t){return new Pt({buffer:Ot(t).buffer})}static fromHex(t){return new Pt({buffer:vt(t).buffer})}get buffer(){return this.bytes.buffer}get latin1(){return Object.defineProperty(this,"latin1",{enumerable:!0,writable:!1,configurable:!1,value:Tt(this.bytes)}),this.latin1}get utf8(){return Object.defineProperty(this,"utf8",{enumerable:!0,writable:!1,configurable:!1,value:Gt(this.bytes)}),this.utf8}get base32(){return Object.defineProperty(this,"base32",{enumerable:!0,writable:!1,configurable:!1,value:Ct(this.bytes)}),this.base32}get hex(){return Object.defineProperty(this,"hex",{enumerable:!0,writable:!1,configurable:!1,
|
||||
value:kt(this.bytes)}),this.hex}constructor({buffer:t,size:e=20}={}){this.bytes=void 0===t?(t=>{if(Et.crypto?.getRandomValues)return Et.crypto.getRandomValues(new Uint8Array(t));throw new Error("Cryptography API not available")})(e):new Uint8Array(t),Object.defineProperty(this,"bytes",{enumerable:!0,writable:!1,configurable:!1,value:this.bytes})}}class jt{static get defaults(){return{issuer:"",label:"OTPAuth",issuerInLabel:!0,algorithm:"SHA1",digits:6,counter:0,window:1}}static generate({secret:t,algorithm:e=jt.defaults.algorithm,digits:s=jt.defaults.digits,counter:i=jt.defaults.counter}){const r=((t,e,s)=>{if(g){const i=Ut[t]??Ut[Bt(t)];return g(i,e,s)}throw new Error("Missing HMAC function")})(e,t.bytes,(t=>{const e=new ArrayBuffer(8),s=new Uint8Array(e);let i=t;for(let t=7;t>=0&&0!==i;t--)s[t]=255&i,i-=s[t],i/=256;return s})(i)),n=15&r[r.byteLength-1];return(((127&r[n])<<24|(255&r[n+1])<<16|(255&r[n+2])<<8|255&r[n+3])%10**s).toString().padStart(s,"0")}generate({counter:t=this.counter++}={}){return jt.generate({secret:this.secret,algorithm:this.algorithm,digits:this.digits,counter:t})}static validate({token:t,secret:e,algorithm:s,digits:i=jt.defaults.digits,counter:r=jt.defaults.counter,window:n=jt.defaults.window}){if(t.length!==i)return null;let o=null;const h=n=>{const h=jt.generate({secret:e,algorithm:s,digits:i,counter:n});((t,e)=>{{if(t.length!==e.length)throw new TypeError("Input strings must have the same length");let s=-1,i=0;for(;++s<t.length;)i|=t.charCodeAt(s)^e.charCodeAt(s);return 0===i}})(t,h)&&(o=n-r)};h(r);for(let t=1;t<=n&&null===o&&(h(r-t),null===o)&&(h(r+t),null===o);++t);return o}validate({token:t,counter:e=this.counter,window:s}){return jt.validate({token:t,secret:this.secret,algorithm:this.algorithm,digits:this.digits,counter:e,window:s})}toString(){const t=encodeURIComponent
|
||||
;return"otpauth://hotp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`counter=${t(this.counter)}`}constructor({issuer:t=jt.defaults.issuer,label:e=jt.defaults.label,issuerInLabel:s=jt.defaults.issuerInLabel,secret:i=new Pt,algorithm:r=jt.defaults.algorithm,digits:n=jt.defaults.digits,counter:o=jt.defaults.counter}={}){this.issuer=t,this.label=e,this.issuerInLabel=s,this.secret="string"==typeof i?Pt.fromBase32(i):i,this.algorithm=Bt(r),this.digits=n,this.counter=o}}class Mt{static get defaults(){return{issuer:"",label:"OTPAuth",issuerInLabel:!0,algorithm:"SHA1",digits:6,period:30,window:1}}static counter({period:t=Mt.defaults.period,timestamp:e=Date.now()}={}){return Math.floor(e/1e3/t)}counter({timestamp:t=Date.now()}={}){return Mt.counter({period:this.period,timestamp:t})}static remaining({period:t=Mt.defaults.period,timestamp:e=Date.now()}={}){return 1e3*t-e%(1e3*t)}remaining({timestamp:t=Date.now()}={}){return Mt.remaining({period:this.period,timestamp:t})}static generate({secret:t,algorithm:e,digits:s,period:i=Mt.defaults.period,timestamp:r=Date.now()}){return jt.generate({secret:t,algorithm:e,digits:s,counter:Mt.counter({period:i,timestamp:r})})}generate({timestamp:t=Date.now()}={}){return Mt.generate({secret:this.secret,algorithm:this.algorithm,digits:this.digits,period:this.period,timestamp:t})}static validate({token:t,secret:e,algorithm:s,digits:i,period:r=Mt.defaults.period,timestamp:n=Date.now(),window:o}){return jt.validate({token:t,secret:e,algorithm:s,digits:i,counter:Mt.counter({period:r,timestamp:n}),window:o})}validate({token:t,timestamp:e,window:s}){return Mt.validate({token:t,secret:this.secret,algorithm:this.algorithm,digits:this.digits,period:this.period,timestamp:e,window:s})}toString(){const t=encodeURIComponent
|
||||
;return"otpauth://totp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`period=${t(this.period)}`}constructor({issuer:t=Mt.defaults.issuer,label:e=Mt.defaults.label,issuerInLabel:s=Mt.defaults.issuerInLabel,secret:i=new Pt,algorithm:r=Mt.defaults.algorithm,digits:n=Mt.defaults.digits,period:o=Mt.defaults.period}={}){this.issuer=t,this.label=e,this.issuerInLabel=s,this.secret="string"==typeof i?Pt.fromBase32(i):i,this.algorithm=Bt(r),this.digits=n,this.period=o}}const Rt=/^otpauth:\/\/([ht]otp)\/(.+)\?([A-Z0-9.~_-]+=[^?&]*(?:&[A-Z0-9.~_-]+=[^?&]*)*)$/i,Nt=/^[2-7A-Z]+=*$/i,Xt=/^SHA(?:1|224|256|384|512|3-224|3-256|3-384|3-512)$/i,Vt=/^[+-]?\d+$/,Zt=/^\+?[1-9]\d*$/;t.HOTP=jt,t.Secret=Pt,t.TOTP=Mt,t.URI=class{static parse(t){let e;try{e=t.match(Rt)}catch(t){}if(!Array.isArray(e))throw new URIError("Invalid URI format");const s=e[1].toLowerCase(),i=e[2].split(/(?::|%3A) *(.+)/i,2).map(decodeURIComponent),r=e[3].split("&").reduce((t,e)=>{const s=e.split(/=(.*)/,2).map(decodeURIComponent),i=s[0].toLowerCase(),r=s[1],n=t;return n[i]=r,n},{});let n;const o={};if("hotp"===s){if(n=jt,void 0===r.counter||!Vt.test(r.counter))throw new TypeError("Missing or invalid 'counter' parameter");o.counter=parseInt(r.counter,10)}else{if("totp"!==s)throw new TypeError("Unknown OTP type");if(n=Mt,void 0!==r.period){if(!Zt.test(r.period))throw new TypeError("Invalid 'period' parameter");o.period=parseInt(r.period,10)}}if(void 0!==r.issuer&&(o.issuer=r.issuer),2===i.length?(o.label=i[1],void 0===o.issuer||""===o.issuer?o.issuer=i[0]:""===i[0]&&(o.issuerInLabel=!1)):(o.label=i[0],void 0!==o.issuer&&""!==o.issuer&&(o.issuerInLabel=!1)),void 0===r.secret||!Nt.test(r.secret))throw new TypeError("Missing or invalid 'secret' parameter");if(o.secret=r.secret,void 0!==r.algorithm){
|
||||
if(!Xt.test(r.algorithm))throw new TypeError("Invalid 'algorithm' parameter");o.algorithm=r.algorithm}if(void 0!==r.digits){if(!Zt.test(r.digits))throw new TypeError("Invalid 'digits' parameter");o.digits=parseInt(r.digits,10)}return new n(o)}static stringify(t){if(t instanceof jt||t instanceof Mt)return t.toString();throw new TypeError("Invalid 'HOTP/TOTP' object")}},t.version="9.4.1"});
|
||||
//# sourceMappingURL=otpauth.umd.min.js.map
|
||||
|
||||
1
web/assets/otpauth/otpauth.umd.min.js.map
Normal file
1
web/assets/otpauth/otpauth.umd.min.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -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
6
web/assets/vue/vue.esm.browser.min.js
vendored
6
web/assets/vue/vue.esm.browser.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
11932
web/assets/vue/vue.js
11932
web/assets/vue/vue.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
6
web/assets/vue/vue.runtime.min.js
vendored
6
web/assets/vue/vue.runtime.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -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
|
||||
@@ -1,59 +1,58 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"x-ui/web/service"
|
||||
"net/http"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// APIController handles the main API routes for the 3x-ui panel, including inbounds and server management.
|
||||
type APIController struct {
|
||||
BaseController
|
||||
inboundController *InboundController
|
||||
serverController *ServerController
|
||||
Tgbot service.Tgbot
|
||||
}
|
||||
|
||||
// NewAPIController creates a new APIController instance and initializes its routes.
|
||||
func NewAPIController(g *gin.RouterGroup) *APIController {
|
||||
a := &APIController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/panel/api/inbounds")
|
||||
g.Use(a.checkLogin)
|
||||
|
||||
a.inboundController = NewInboundController(g)
|
||||
|
||||
inboundRoutes := []struct {
|
||||
Method string
|
||||
Path string
|
||||
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 {
|
||||
g.Handle(route.Method, route.Path, route.Handler)
|
||||
// checkAPIAuth is a middleware that returns 404 for unauthenticated API requests
|
||||
// to hide the existence of API endpoints from unauthorized users
|
||||
func (a *APIController) checkAPIAuth(c *gin.Context) {
|
||||
if !session.IsLogin(c) {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func (a *APIController) createBackup(c *gin.Context) {
|
||||
// initRouter sets up the API routes for inbounds, server, and other endpoints.
|
||||
func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||
// Main API group
|
||||
api := g.Group("/panel/api")
|
||||
api.Use(a.checkAPIAuth)
|
||||
|
||||
// Inbounds API
|
||||
inbounds := api.Group("/inbounds")
|
||||
a.inboundController = NewInboundController(inbounds)
|
||||
|
||||
// Server API
|
||||
server := api.Group("/server")
|
||||
a.serverController = NewServerController(server)
|
||||
|
||||
// Extra routes
|
||||
api.GET("/backuptotgbot", a.BackuptoTgbot)
|
||||
}
|
||||
|
||||
// BackuptoTgbot sends a backup of the panel data to Telegram bot admins.
|
||||
func (a *APIController) BackuptoTgbot(c *gin.Context) {
|
||||
a.Tgbot.SendBackupToAdmins()
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
// Package controller provides HTTP request handlers and controllers for the 3x-ui web management panel.
|
||||
// It handles routing, authentication, and API endpoints for managing Xray inbounds, settings, and more.
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"x-ui/logger"
|
||||
"x-ui/web/locale"
|
||||
"x-ui/web/session"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/locale"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// BaseController provides common functionality for all controllers, including authentication checks.
|
||||
type BaseController struct{}
|
||||
|
||||
// checkLogin is a middleware that verifies user authentication and handles unauthorized access.
|
||||
func (a *BaseController) checkLogin(c *gin.Context) {
|
||||
if !session.IsLogin(c) {
|
||||
if isAjax(c) {
|
||||
@@ -25,6 +29,7 @@ func (a *BaseController) checkLogin(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// I18nWeb retrieves an internationalized message for the web interface based on the current locale.
|
||||
func I18nWeb(c *gin.Context, name string, params ...string) string {
|
||||
anyfunc, funcExists := c.Get("I18n")
|
||||
if !funcExists {
|
||||
|
||||
@@ -5,28 +5,35 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"x-ui/database/model"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/websocket"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// InboundController handles HTTP requests related to Xray inbounds management.
|
||||
type InboundController struct {
|
||||
inboundService service.InboundService
|
||||
xrayService service.XrayService
|
||||
}
|
||||
|
||||
// NewInboundController creates a new InboundController and sets up its routes.
|
||||
func NewInboundController(g *gin.RouterGroup) *InboundController {
|
||||
a := &InboundController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
// initRouter initializes the routes for inbound-related operations.
|
||||
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("/del/:id", a.delInbound)
|
||||
g.POST("/update/:id", a.updateInbound)
|
||||
@@ -41,8 +48,12 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||
g.POST("/import", a.importInbound)
|
||||
g.POST("/onlines", a.onlines)
|
||||
g.POST("/lastOnline", a.lastOnline)
|
||||
g.POST("/updateClientTraffic/:email", a.updateClientTraffic)
|
||||
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
|
||||
}
|
||||
|
||||
// getInbounds retrieves the list of inbounds for the logged-in user.
|
||||
func (a *InboundController) getInbounds(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
inbounds, err := a.inboundService.GetInbounds(user.Id)
|
||||
@@ -53,6 +64,7 @@ func (a *InboundController) getInbounds(c *gin.Context) {
|
||||
jsonObj(c, inbounds, nil)
|
||||
}
|
||||
|
||||
// getInbound retrieves a specific inbound by its ID.
|
||||
func (a *InboundController) getInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -67,6 +79,7 @@ func (a *InboundController) getInbound(c *gin.Context) {
|
||||
jsonObj(c, inbound, nil)
|
||||
}
|
||||
|
||||
// getClientTraffics retrieves client traffic information by email.
|
||||
func (a *InboundController) getClientTraffics(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
|
||||
@@ -77,6 +90,7 @@ func (a *InboundController) getClientTraffics(c *gin.Context) {
|
||||
jsonObj(c, clientTraffics, nil)
|
||||
}
|
||||
|
||||
// getClientTrafficsById retrieves client traffic information by inbound ID.
|
||||
func (a *InboundController) getClientTrafficsById(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
clientTraffics, err := a.inboundService.GetClientTrafficByID(id)
|
||||
@@ -87,6 +101,7 @@ func (a *InboundController) getClientTrafficsById(c *gin.Context) {
|
||||
jsonObj(c, clientTraffics, nil)
|
||||
}
|
||||
|
||||
// addInbound creates a new inbound configuration.
|
||||
func (a *InboundController) addInbound(c *gin.Context) {
|
||||
inbound := &model.Inbound{}
|
||||
err := c.ShouldBind(inbound)
|
||||
@@ -102,36 +117,43 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
||||
inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
|
||||
}
|
||||
|
||||
needRestart := false
|
||||
inbound, needRestart, err = a.inboundService.AddInbound(inbound)
|
||||
inbound, needRestart, err := a.inboundService.AddInbound(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, err)
|
||||
if err == nil && needRestart {
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
// Broadcast inbounds update via WebSocket
|
||||
inbounds, _ := a.inboundService.GetInbounds(user.Id)
|
||||
websocket.BroadcastInbounds(inbounds)
|
||||
}
|
||||
|
||||
// delInbound deletes an inbound configuration by its ID.
|
||||
func (a *InboundController) delInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), err)
|
||||
return
|
||||
}
|
||||
needRestart := true
|
||||
needRestart, err = a.inboundService.DelInbound(id)
|
||||
needRestart, err := a.inboundService.DelInbound(id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, err)
|
||||
if err == nil && needRestart {
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
// Broadcast inbounds update via WebSocket
|
||||
user := session.GetLoginUser(c)
|
||||
inbounds, _ := a.inboundService.GetInbounds(user.Id)
|
||||
websocket.BroadcastInbounds(inbounds)
|
||||
}
|
||||
|
||||
// updateInbound updates an existing inbound configuration.
|
||||
func (a *InboundController) updateInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -146,18 +168,22 @@ func (a *InboundController) updateInbound(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
||||
return
|
||||
}
|
||||
needRestart := true
|
||||
inbound, needRestart, err = a.inboundService.UpdateInbound(inbound)
|
||||
inbound, needRestart, err := a.inboundService.UpdateInbound(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, err)
|
||||
if err == nil && needRestart {
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
// Broadcast inbounds update via WebSocket
|
||||
user := session.GetLoginUser(c)
|
||||
inbounds, _ := a.inboundService.GetInbounds(user.Id)
|
||||
websocket.BroadcastInbounds(inbounds)
|
||||
}
|
||||
|
||||
// getClientIps retrieves the IP addresses associated with a client by email.
|
||||
func (a *InboundController) getClientIps(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
@@ -170,6 +196,7 @@ func (a *InboundController) getClientIps(c *gin.Context) {
|
||||
jsonObj(c, ips, nil)
|
||||
}
|
||||
|
||||
// clearClientIps clears the IP addresses for a client by email.
|
||||
func (a *InboundController) clearClientIps(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
@@ -181,6 +208,7 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.logCleanSuccess"), nil)
|
||||
}
|
||||
|
||||
// addInboundClient adds a new client to an existing inbound.
|
||||
func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||
data := &model.Inbound{}
|
||||
err := c.ShouldBind(data)
|
||||
@@ -189,9 +217,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
needRestart := true
|
||||
|
||||
needRestart, err = a.inboundService.AddInboundClient(data)
|
||||
needRestart, err := a.inboundService.AddInboundClient(data)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
@@ -202,6 +228,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// delInboundClient deletes a client from an inbound by inbound ID and client ID.
|
||||
func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -210,9 +237,7 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||
}
|
||||
clientId := c.Param("clientId")
|
||||
|
||||
needRestart := true
|
||||
|
||||
needRestart, err = a.inboundService.DelInboundClient(id, clientId)
|
||||
needRestart, err := a.inboundService.DelInboundClient(id, clientId)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
@@ -223,6 +248,7 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// updateInboundClient updates a client's configuration in an inbound.
|
||||
func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||
clientId := c.Param("clientId")
|
||||
|
||||
@@ -233,9 +259,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
needRestart := true
|
||||
|
||||
needRestart, err = a.inboundService.UpdateInboundClient(inbound, clientId)
|
||||
needRestart, err := a.inboundService.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
@@ -246,6 +270,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// resetClientTraffic resets the traffic counter for a specific client in an inbound.
|
||||
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -265,6 +290,7 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// resetAllTraffics resets all traffic counters across all inbounds.
|
||||
func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
||||
err := a.inboundService.ResetAllTraffics()
|
||||
if err != nil {
|
||||
@@ -276,6 +302,7 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllTrafficSuccess"), nil)
|
||||
}
|
||||
|
||||
// resetAllClientTraffics resets traffic counters for all clients in a specific inbound.
|
||||
func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -293,6 +320,7 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil)
|
||||
}
|
||||
|
||||
// importInbound imports an inbound configuration from provided data.
|
||||
func (a *InboundController) importInbound(c *gin.Context) {
|
||||
inbound := &model.Inbound{}
|
||||
err := json.Unmarshal([]byte(c.PostForm("data")), inbound)
|
||||
@@ -322,6 +350,7 @@ func (a *InboundController) importInbound(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// delDepletedClients deletes clients in an inbound who have exhausted their traffic limits.
|
||||
func (a *InboundController) delDepletedClients(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -336,6 +365,60 @@ func (a *InboundController) delDepletedClients(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.delDepletedClientsSuccess"), nil)
|
||||
}
|
||||
|
||||
// onlines retrieves the list of currently online clients.
|
||||
func (a *InboundController) onlines(c *gin.Context) {
|
||||
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
||||
}
|
||||
|
||||
// lastOnline retrieves the last online timestamps for clients.
|
||||
func (a *InboundController) lastOnline(c *gin.Context) {
|
||||
data, err := a.inboundService.GetClientsLastOnline()
|
||||
jsonObj(c, data, err)
|
||||
}
|
||||
|
||||
// updateClientTraffic updates the traffic statistics for a client by email.
|
||||
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)
|
||||
}
|
||||
|
||||
// delInboundClientByEmail deletes a client from an inbound by email address.
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,20 +5,22 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"x-ui/logger"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// LoginForm represents the login request structure.
|
||||
type LoginForm struct {
|
||||
Username string `json:"username" form:"username"`
|
||||
Password string `json:"password" form:"password"`
|
||||
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
|
||||
Username string `json:"username" form:"username"`
|
||||
Password string `json:"password" form:"password"`
|
||||
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
|
||||
}
|
||||
|
||||
// IndexController handles the main index and login-related routes.
|
||||
type IndexController struct {
|
||||
BaseController
|
||||
|
||||
@@ -27,19 +29,23 @@ type IndexController struct {
|
||||
tgbot service.Tgbot
|
||||
}
|
||||
|
||||
// NewIndexController creates a new IndexController and initializes its routes.
|
||||
func NewIndexController(g *gin.RouterGroup) *IndexController {
|
||||
a := &IndexController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
// initRouter sets up the routes for index, login, logout, and two-factor authentication.
|
||||
func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/", a.index)
|
||||
g.POST("/login", a.login)
|
||||
g.GET("/logout", a.logout)
|
||||
|
||||
g.POST("/login", a.login)
|
||||
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
|
||||
}
|
||||
|
||||
// index handles the root route, redirecting logged-in users to the panel or showing the login page.
|
||||
func (a *IndexController) index(c *gin.Context) {
|
||||
if session.IsLogin(c) {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "panel/")
|
||||
@@ -48,6 +54,7 @@ func (a *IndexController) index(c *gin.Context) {
|
||||
html(c, "login.html", "pages.login.title", nil)
|
||||
}
|
||||
|
||||
// login handles user authentication and session creation.
|
||||
func (a *IndexController) login(c *gin.Context) {
|
||||
var form LoginForm
|
||||
|
||||
@@ -95,6 +102,7 @@ func (a *IndexController) login(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil)
|
||||
}
|
||||
|
||||
// logout handles user logout by clearing the session and redirecting to the login page.
|
||||
func (a *IndexController) logout(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
if user != nil {
|
||||
@@ -107,6 +115,7 @@ func (a *IndexController) logout(c *gin.Context) {
|
||||
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
||||
}
|
||||
|
||||
// getTwoFactorEnable retrieves the current status of two-factor authentication.
|
||||
func (a *IndexController) getTwoFactorEnable(c *gin.Context) {
|
||||
status, err := a.settingService.GetTwoFactorEnable()
|
||||
if err == nil {
|
||||
|
||||
@@ -4,79 +4,117 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/global"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/websocket"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
|
||||
|
||||
// ServerController handles server management and status-related operations.
|
||||
type ServerController struct {
|
||||
BaseController
|
||||
|
||||
serverService service.ServerService
|
||||
serverService service.ServerService
|
||||
settingService service.SettingService
|
||||
|
||||
lastStatus *service.Status
|
||||
lastGetStatusTime time.Time
|
||||
lastStatus *service.Status
|
||||
|
||||
lastVersions []string
|
||||
lastGetVersionsTime time.Time
|
||||
lastGetVersionsTime int64 // unix seconds
|
||||
}
|
||||
|
||||
// NewServerController creates a new ServerController, initializes routes, and starts background tasks.
|
||||
func NewServerController(g *gin.RouterGroup) *ServerController {
|
||||
a := &ServerController{
|
||||
lastGetStatusTime: time.Now(),
|
||||
}
|
||||
a := &ServerController{}
|
||||
a.initRouter(g)
|
||||
a.startTask()
|
||||
return a
|
||||
}
|
||||
|
||||
// initRouter sets up the routes for server status, Xray management, and utility endpoints.
|
||||
func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/server")
|
||||
|
||||
g.Use(a.checkLogin)
|
||||
g.POST("/status", a.status)
|
||||
g.POST("/getXrayVersion", a.getXrayVersion)
|
||||
g.GET("/status", a.status)
|
||||
g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket)
|
||||
g.GET("/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("/restartXrayService", a.restartXrayService)
|
||||
g.POST("/installXray/:version", a.installXray)
|
||||
g.POST("/updateGeofile", a.updateGeofile)
|
||||
g.POST("/updateGeofile/:fileName", a.updateGeofile)
|
||||
g.POST("/logs/:count", a.getLogs)
|
||||
g.POST("/getConfigJson", a.getConfigJson)
|
||||
g.GET("/getDb", a.getDb)
|
||||
g.POST("/xraylogs/:count", a.getXrayLogs)
|
||||
g.POST("/importDB", a.importDB)
|
||||
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
|
||||
g.POST("/getNewEchCert", a.getNewEchCert)
|
||||
}
|
||||
|
||||
// refreshStatus updates the cached server status and collects CPU history.
|
||||
func (a *ServerController) refreshStatus() {
|
||||
a.lastStatus = a.serverService.GetStatus(a.lastStatus)
|
||||
// collect cpu history when status is fresh
|
||||
if a.lastStatus != nil {
|
||||
a.serverService.AppendCpuSample(time.Now(), a.lastStatus.Cpu)
|
||||
// Broadcast status update via WebSocket
|
||||
websocket.BroadcastStatus(a.lastStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// startTask initiates background tasks for continuous status monitoring.
|
||||
func (a *ServerController) startTask() {
|
||||
webServer := global.GetWebServer()
|
||||
c := webServer.GetCron()
|
||||
c.AddFunc("@every 2s", func() {
|
||||
now := time.Now()
|
||||
if now.Sub(a.lastGetStatusTime) > time.Minute*3 {
|
||||
return
|
||||
}
|
||||
// Always refresh to keep CPU history collected continuously.
|
||||
// Sampling is lightweight and capped to ~6 hours in memory.
|
||||
a.refreshStatus()
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ServerController) status(c *gin.Context) {
|
||||
a.lastGetStatusTime = time.Now()
|
||||
// status returns the current server status information.
|
||||
func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) }
|
||||
|
||||
jsonObj(c, a.lastStatus, nil)
|
||||
// getCpuHistoryBucket retrieves aggregated CPU usage history based on the specified time bucket.
|
||||
func (a *ServerController) getCpuHistoryBucket(c *gin.Context) {
|
||||
bucketStr := c.Param("bucket")
|
||||
bucket, err := strconv.Atoi(bucketStr)
|
||||
if err != nil || bucket <= 0 {
|
||||
jsonMsg(c, "invalid bucket", fmt.Errorf("bad bucket"))
|
||||
return
|
||||
}
|
||||
allowed := map[int]bool{
|
||||
2: true, // Real-time view
|
||||
30: true, // 30s intervals
|
||||
60: true, // 1m intervals
|
||||
120: true, // 2m intervals
|
||||
180: true, // 3m intervals
|
||||
300: true, // 5m intervals
|
||||
}
|
||||
if !allowed[bucket] {
|
||||
jsonMsg(c, "invalid bucket", fmt.Errorf("unsupported bucket"))
|
||||
return
|
||||
}
|
||||
points := a.serverService.AggregateCpuHistory(bucket, 60)
|
||||
jsonObj(c, points, nil)
|
||||
}
|
||||
|
||||
// getXrayVersion retrieves available Xray versions, with caching for 1 minute.
|
||||
func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||
now := time.Now()
|
||||
if now.Sub(a.lastGetVersionsTime) <= time.Minute {
|
||||
now := time.Now().Unix()
|
||||
if now-a.lastGetVersionsTime <= 60 { // 1 minute cache
|
||||
jsonObj(c, a.lastVersions, nil)
|
||||
return
|
||||
}
|
||||
@@ -88,42 +126,68 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||
}
|
||||
|
||||
a.lastVersions = versions
|
||||
a.lastGetVersionsTime = time.Now()
|
||||
a.lastGetVersionsTime = now
|
||||
|
||||
jsonObj(c, versions, nil)
|
||||
}
|
||||
|
||||
// installXray installs or updates Xray to the specified version.
|
||||
func (a *ServerController) installXray(c *gin.Context) {
|
||||
version := c.Param("version")
|
||||
err := a.serverService.UpdateXray(version)
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err)
|
||||
}
|
||||
|
||||
// updateGeofile updates the specified geo file for Xray.
|
||||
func (a *ServerController) updateGeofile(c *gin.Context) {
|
||||
fileName := c.Param("fileName")
|
||||
|
||||
// Validate the filename for security (prevent path traversal attacks)
|
||||
if fileName != "" && !a.serverService.IsValidGeofileName(fileName) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"),
|
||||
fmt.Errorf("invalid filename: contains unsafe characters or path traversal patterns"))
|
||||
return
|
||||
}
|
||||
|
||||
err := a.serverService.UpdateGeofile(fileName)
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err)
|
||||
}
|
||||
|
||||
// stopXrayService stops the Xray service.
|
||||
func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||
a.lastGetStatusTime = time.Now()
|
||||
err := a.serverService.StopXrayService()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err)
|
||||
websocket.BroadcastXrayState("error", err.Error())
|
||||
return
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.xray.stopSuccess"), err)
|
||||
websocket.BroadcastXrayState("stop", "")
|
||||
websocket.BroadcastNotification(
|
||||
I18nWeb(c, "pages.xray.stopSuccess"),
|
||||
"Xray service has been stopped",
|
||||
"warning",
|
||||
)
|
||||
}
|
||||
|
||||
// restartXrayService restarts the Xray service.
|
||||
func (a *ServerController) restartXrayService(c *gin.Context) {
|
||||
err := a.serverService.RestartXrayService()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.xray.restartError"), err)
|
||||
websocket.BroadcastXrayState("error", err.Error())
|
||||
return
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.xray.restartSuccess"), err)
|
||||
websocket.BroadcastXrayState("running", "")
|
||||
websocket.BroadcastNotification(
|
||||
I18nWeb(c, "pages.xray.restartSuccess"),
|
||||
"Xray service has been restarted successfully",
|
||||
"success",
|
||||
)
|
||||
}
|
||||
|
||||
// getLogs retrieves the application logs based on count, level, and syslog filters.
|
||||
func (a *ServerController) getLogs(c *gin.Context) {
|
||||
count := c.Param("count")
|
||||
level := c.PostForm("level")
|
||||
@@ -132,6 +196,52 @@ func (a *ServerController) getLogs(c *gin.Context) {
|
||||
jsonObj(c, logs, nil)
|
||||
}
|
||||
|
||||
// getXrayLogs retrieves Xray logs with filtering options for direct, blocked, and proxy traffic.
|
||||
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)
|
||||
}
|
||||
|
||||
// getConfigJson retrieves the Xray configuration as JSON.
|
||||
func (a *ServerController) getConfigJson(c *gin.Context) {
|
||||
configJson, err := a.serverService.GetConfigJson()
|
||||
if err != nil {
|
||||
@@ -141,6 +251,7 @@ func (a *ServerController) getConfigJson(c *gin.Context) {
|
||||
jsonObj(c, configJson, nil)
|
||||
}
|
||||
|
||||
// getDb downloads the database file.
|
||||
func (a *ServerController) getDb(c *gin.Context) {
|
||||
db, err := a.serverService.GetDb()
|
||||
if err != nil {
|
||||
@@ -168,6 +279,7 @@ func isValidFilename(filename string) bool {
|
||||
return filenameRegex.MatchString(filename)
|
||||
}
|
||||
|
||||
// importDB imports a database file and restarts the Xray service.
|
||||
func (a *ServerController) importDB(c *gin.Context) {
|
||||
// Get the file from the request body
|
||||
file, _, err := c.Request.FormFile("db")
|
||||
@@ -178,9 +290,7 @@ func (a *ServerController) importDB(c *gin.Context) {
|
||||
defer file.Close()
|
||||
// Always restart Xray before return
|
||||
defer a.serverService.RestartXrayService()
|
||||
defer func() {
|
||||
a.lastGetStatusTime = time.Now()
|
||||
}()
|
||||
// lastGetStatusTime removed; no longer needed
|
||||
// Import it
|
||||
err = a.serverService.ImportDB(file)
|
||||
if err != nil {
|
||||
@@ -190,6 +300,7 @@ func (a *ServerController) importDB(c *gin.Context) {
|
||||
jsonObj(c, I18nWeb(c, "pages.index.importDatabaseSuccess"), nil)
|
||||
}
|
||||
|
||||
// getNewX25519Cert generates a new X25519 certificate.
|
||||
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
|
||||
cert, err := a.serverService.GetNewX25519Cert()
|
||||
if err != nil {
|
||||
@@ -198,3 +309,55 @@ func (a *ServerController) getNewX25519Cert(c *gin.Context) {
|
||||
}
|
||||
jsonObj(c, cert, nil)
|
||||
}
|
||||
|
||||
// getNewmldsa65 generates a new ML-DSA-65 key.
|
||||
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)
|
||||
}
|
||||
|
||||
// getNewEchCert generates a new ECH certificate for the given SNI.
|
||||
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)
|
||||
}
|
||||
|
||||
// getNewVlessEnc generates a new VLESS encryption key.
|
||||
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)
|
||||
}
|
||||
|
||||
// getNewUUID generates a new UUID.
|
||||
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)
|
||||
}
|
||||
|
||||
// getNewmlkem768 generates a new ML-KEM-768 key.
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"x-ui/util/crypto"
|
||||
"x-ui/web/entity"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/entity"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// updateUserForm represents the form for updating user credentials.
|
||||
type updateUserForm struct {
|
||||
OldUsername string `json:"oldUsername" form:"oldUsername"`
|
||||
OldPassword string `json:"oldPassword" form:"oldPassword"`
|
||||
@@ -19,18 +20,21 @@ type updateUserForm struct {
|
||||
NewPassword string `json:"newPassword" form:"newPassword"`
|
||||
}
|
||||
|
||||
// SettingController handles settings and user management operations.
|
||||
type SettingController struct {
|
||||
settingService service.SettingService
|
||||
userService service.UserService
|
||||
panelService service.PanelService
|
||||
}
|
||||
|
||||
// NewSettingController creates a new SettingController and initializes its routes.
|
||||
func NewSettingController(g *gin.RouterGroup) *SettingController {
|
||||
a := &SettingController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
// initRouter sets up the routes for settings management.
|
||||
func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/setting")
|
||||
|
||||
@@ -42,6 +46,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||
}
|
||||
|
||||
// getAllSetting retrieves all current settings.
|
||||
func (a *SettingController) getAllSetting(c *gin.Context) {
|
||||
allSetting, err := a.settingService.GetAllSetting()
|
||||
if err != nil {
|
||||
@@ -51,6 +56,7 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
|
||||
jsonObj(c, allSetting, nil)
|
||||
}
|
||||
|
||||
// getDefaultSettings retrieves the default settings based on the host.
|
||||
func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
||||
result, err := a.settingService.GetDefaultSettings(c.Request.Host)
|
||||
if err != nil {
|
||||
@@ -60,6 +66,7 @@ func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
||||
jsonObj(c, result, nil)
|
||||
}
|
||||
|
||||
// updateSetting updates all settings with the provided data.
|
||||
func (a *SettingController) updateSetting(c *gin.Context) {
|
||||
allSetting := &entity.AllSetting{}
|
||||
err := c.ShouldBind(allSetting)
|
||||
@@ -71,6 +78,7 @@ func (a *SettingController) updateSetting(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
}
|
||||
|
||||
// updateUser updates the current user's username and password.
|
||||
func (a *SettingController) updateUser(c *gin.Context) {
|
||||
form := &updateUserForm{}
|
||||
err := c.ShouldBind(form)
|
||||
@@ -96,11 +104,13 @@ func (a *SettingController) updateUser(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
|
||||
}
|
||||
|
||||
// restartPanel restarts the panel service after a delay.
|
||||
func (a *SettingController) restartPanel(c *gin.Context) {
|
||||
err := a.panelService.RestartPanel(time.Second * 3)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.restartPanelSuccess"), err)
|
||||
}
|
||||
|
||||
// getDefaultXrayConfig retrieves the default Xray configuration.
|
||||
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
|
||||
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
|
||||
if err != nil {
|
||||
|
||||
@@ -5,13 +5,14 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/entity"
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/entity"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// getRemoteIp extracts the real IP address from the request headers or remote address.
|
||||
func getRemoteIp(c *gin.Context) string {
|
||||
value := c.GetHeader("X-Real-IP")
|
||||
if value != "" {
|
||||
@@ -27,14 +28,17 @@ func getRemoteIp(c *gin.Context) string {
|
||||
return ip
|
||||
}
|
||||
|
||||
// jsonMsg sends a JSON response with a message and error status.
|
||||
func jsonMsg(c *gin.Context, msg string, err error) {
|
||||
jsonMsgObj(c, msg, nil, err)
|
||||
}
|
||||
|
||||
// jsonObj sends a JSON response with an object and error status.
|
||||
func jsonObj(c *gin.Context, obj any, err error) {
|
||||
jsonMsgObj(c, "", obj, err)
|
||||
}
|
||||
|
||||
// jsonMsgObj sends a JSON response with a message, object, and error status.
|
||||
func jsonMsgObj(c *gin.Context, msg string, obj any, err error) {
|
||||
m := entity.Msg{
|
||||
Obj: obj,
|
||||
@@ -52,6 +56,7 @@ func jsonMsgObj(c *gin.Context, msg string, obj any, err error) {
|
||||
c.JSON(http.StatusOK, m)
|
||||
}
|
||||
|
||||
// pureJsonMsg sends a pure JSON message response with custom status code.
|
||||
func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) {
|
||||
c.JSON(statusCode, entity.Msg{
|
||||
Success: success,
|
||||
@@ -59,6 +64,7 @@ func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) {
|
||||
})
|
||||
}
|
||||
|
||||
// html renders an HTML template with the provided data and title.
|
||||
func html(c *gin.Context, name string, title string, data gin.H) {
|
||||
if data == nil {
|
||||
data = gin.H{}
|
||||
@@ -81,6 +87,7 @@ func html(c *gin.Context, name string, title string, data gin.H) {
|
||||
c.HTML(http.StatusOK, name, getContext(data))
|
||||
}
|
||||
|
||||
// getContext adds version and other context data to the provided gin.H.
|
||||
func getContext(h gin.H) gin.H {
|
||||
a := gin.H{
|
||||
"cur_ver": config.GetVersion(),
|
||||
@@ -91,6 +98,7 @@ func getContext(h gin.H) gin.H {
|
||||
return a
|
||||
}
|
||||
|
||||
// isAjax checks if the request is an AJAX request.
|
||||
func isAjax(c *gin.Context) bool {
|
||||
return c.GetHeader("X-Requested-With") == "XMLHttpRequest"
|
||||
}
|
||||
|
||||
189
web/controller/websocket.go
Normal file
189
web/controller/websocket.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/websocket"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
ws "github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write a message to the peer
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Time allowed to read the next pong message from the peer
|
||||
pongWait = 60 * time.Second
|
||||
|
||||
// Send pings to peer with this period (must be less than pongWait)
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
|
||||
// Maximum message size allowed from peer
|
||||
maxMessageSize = 512
|
||||
)
|
||||
|
||||
var upgrader = ws.Upgrader{
|
||||
ReadBufferSize: 4096, // Increased from 1024 for better performance
|
||||
WriteBufferSize: 4096, // Increased from 1024 for better performance
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// Check origin for security
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
// Allow connections without Origin header (same-origin requests)
|
||||
return true
|
||||
}
|
||||
// Get the host from the request
|
||||
host := r.Host
|
||||
// Extract scheme and host from origin
|
||||
originURL := origin
|
||||
// Simple check: origin should match the request host
|
||||
// This prevents cross-origin WebSocket hijacking
|
||||
if strings.HasPrefix(originURL, "http://") || strings.HasPrefix(originURL, "https://") {
|
||||
// Extract host from origin
|
||||
originHost := strings.TrimPrefix(strings.TrimPrefix(originURL, "http://"), "https://")
|
||||
if idx := strings.Index(originHost, "/"); idx != -1 {
|
||||
originHost = originHost[:idx]
|
||||
}
|
||||
if idx := strings.Index(originHost, ":"); idx != -1 {
|
||||
originHost = originHost[:idx]
|
||||
}
|
||||
// Compare hosts (without port)
|
||||
requestHost := host
|
||||
if idx := strings.Index(requestHost, ":"); idx != -1 {
|
||||
requestHost = requestHost[:idx]
|
||||
}
|
||||
return originHost == requestHost || originHost == "" || requestHost == ""
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
// WebSocketController handles WebSocket connections for real-time updates
|
||||
type WebSocketController struct {
|
||||
BaseController
|
||||
hub *websocket.Hub
|
||||
}
|
||||
|
||||
// NewWebSocketController creates a new WebSocket controller
|
||||
func NewWebSocketController(hub *websocket.Hub) *WebSocketController {
|
||||
return &WebSocketController{
|
||||
hub: hub,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleWebSocket handles WebSocket connections
|
||||
func (w *WebSocketController) HandleWebSocket(c *gin.Context) {
|
||||
// Check authentication
|
||||
if !session.IsLogin(c) {
|
||||
logger.Warningf("Unauthorized WebSocket connection attempt from %s", getRemoteIp(c))
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Upgrade connection to WebSocket
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error("Failed to upgrade WebSocket connection:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create client
|
||||
clientID := uuid.New().String()
|
||||
client := &websocket.Client{
|
||||
ID: clientID,
|
||||
Hub: w.hub,
|
||||
Send: make(chan []byte, 512), // Increased from 256 to 512 to prevent overflow
|
||||
Topics: make(map[websocket.MessageType]bool),
|
||||
}
|
||||
|
||||
// Register client
|
||||
w.hub.Register(client)
|
||||
logger.Debugf("WebSocket client %s registered from %s", clientID, getRemoteIp(c))
|
||||
|
||||
// Start goroutines for reading and writing
|
||||
go w.writePump(client, conn)
|
||||
go w.readPump(client, conn)
|
||||
}
|
||||
|
||||
// readPump pumps messages from the WebSocket connection to the hub
|
||||
func (w *WebSocketController) readPump(client *websocket.Client, conn *ws.Conn) {
|
||||
defer func() {
|
||||
if r := common.Recover("WebSocket readPump panic"); r != nil {
|
||||
logger.Error("WebSocket readPump panic recovered:", r)
|
||||
}
|
||||
w.hub.Unregister(client)
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
conn.SetPongHandler(func(string) error {
|
||||
conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
return nil
|
||||
})
|
||||
conn.SetReadLimit(maxMessageSize)
|
||||
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if ws.IsUnexpectedCloseError(err, ws.CloseGoingAway, ws.CloseAbnormalClosure) {
|
||||
logger.Debugf("WebSocket read error for client %s: %v", client.ID, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Validate message size
|
||||
if len(message) > maxMessageSize {
|
||||
logger.Warningf("WebSocket message from client %s exceeds max size: %d bytes", client.ID, len(message))
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle incoming messages (e.g., subscription requests)
|
||||
// For now, we'll just log them
|
||||
logger.Debugf("Received WebSocket message from client %s: %s", client.ID, string(message))
|
||||
}
|
||||
}
|
||||
|
||||
// writePump pumps messages from the hub to the WebSocket connection
|
||||
func (w *WebSocketController) writePump(client *websocket.Client, conn *ws.Conn) {
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer func() {
|
||||
if r := common.Recover("WebSocket writePump panic"); r != nil {
|
||||
logger.Error("WebSocket writePump panic recovered:", r)
|
||||
}
|
||||
ticker.Stop()
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-client.Send:
|
||||
conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if !ok {
|
||||
// Hub closed the channel
|
||||
conn.WriteMessage(ws.CloseMessage, []byte{})
|
||||
return
|
||||
}
|
||||
|
||||
// Send each message individually (no batching)
|
||||
// This ensures each JSON message is sent separately and can be parsed correctly
|
||||
if err := conn.WriteMessage(ws.TextMessage, message); err != nil {
|
||||
logger.Debugf("WebSocket write error for client %s: %v", client.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := conn.WriteMessage(ws.PingMessage, nil); err != nil {
|
||||
logger.Debugf("WebSocket ping error for client %s: %v", client.ID, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"x-ui/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// XraySettingController handles Xray configuration and settings operations.
|
||||
type XraySettingController struct {
|
||||
XraySettingService service.XraySettingService
|
||||
SettingService service.SettingService
|
||||
@@ -15,24 +16,27 @@ type XraySettingController struct {
|
||||
WarpService service.WarpService
|
||||
}
|
||||
|
||||
// NewXraySettingController creates a new XraySettingController and initializes its routes.
|
||||
func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
|
||||
a := &XraySettingController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
// initRouter sets up the routes for Xray settings management.
|
||||
func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/xray")
|
||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||
g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
|
||||
g.GET("/getXrayResult", a.getXrayResult)
|
||||
|
||||
g.POST("/", a.getXraySetting)
|
||||
g.POST("/update", a.updateSetting)
|
||||
g.GET("/getXrayResult", a.getXrayResult)
|
||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||
g.POST("/warp/:action", a.warp)
|
||||
g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
|
||||
g.POST("/update", a.updateSetting)
|
||||
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
||||
}
|
||||
|
||||
// getXraySetting retrieves the Xray configuration template and inbound tags.
|
||||
func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
||||
xraySetting, err := a.SettingService.GetXrayConfigTemplate()
|
||||
if err != nil {
|
||||
@@ -48,12 +52,14 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
||||
jsonObj(c, xrayResponse, nil)
|
||||
}
|
||||
|
||||
// updateSetting updates the Xray configuration settings.
|
||||
func (a *XraySettingController) updateSetting(c *gin.Context) {
|
||||
xraySetting := c.PostForm("xraySetting")
|
||||
err := a.XraySettingService.SaveXraySetting(xraySetting)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
}
|
||||
|
||||
// getDefaultXrayConfig retrieves the default Xray configuration.
|
||||
func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
|
||||
defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig()
|
||||
if err != nil {
|
||||
@@ -63,10 +69,12 @@ func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
|
||||
jsonObj(c, defaultJsonConfig, nil)
|
||||
}
|
||||
|
||||
// getXrayResult retrieves the current Xray service result.
|
||||
func (a *XraySettingController) getXrayResult(c *gin.Context) {
|
||||
jsonObj(c, a.XrayService.GetXrayResult(), nil)
|
||||
}
|
||||
|
||||
// warp handles Warp-related operations based on the action parameter.
|
||||
func (a *XraySettingController) warp(c *gin.Context) {
|
||||
action := c.Param("action")
|
||||
var resp string
|
||||
@@ -90,6 +98,7 @@ func (a *XraySettingController) warp(c *gin.Context) {
|
||||
jsonObj(c, resp, err)
|
||||
}
|
||||
|
||||
// getOutboundsTraffic retrieves the traffic statistics for outbounds.
|
||||
func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
|
||||
outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
|
||||
if err != nil {
|
||||
@@ -99,6 +108,7 @@ func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
|
||||
jsonObj(c, outboundsTraffic, nil)
|
||||
}
|
||||
|
||||
// resetOutboundsTraffic resets the traffic statistics for the specified outbound tag.
|
||||
func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
|
||||
tag := c.PostForm("tag")
|
||||
err := a.OutboundService.ResetOutboundTraffic(tag)
|
||||
|
||||
@@ -4,20 +4,22 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// XUIController is the main controller for the X-UI panel, managing sub-controllers.
|
||||
type XUIController struct {
|
||||
BaseController
|
||||
|
||||
inboundController *InboundController
|
||||
settingController *SettingController
|
||||
xraySettingController *XraySettingController
|
||||
}
|
||||
|
||||
// NewXUIController creates a new XUIController and initializes its routes.
|
||||
func NewXUIController(g *gin.RouterGroup) *XUIController {
|
||||
a := &XUIController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
// initRouter sets up the main panel routes and initializes sub-controllers.
|
||||
func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/panel")
|
||||
g.Use(a.checkLogin)
|
||||
@@ -27,23 +29,26 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/settings", a.settings)
|
||||
g.GET("/xray", a.xraySettings)
|
||||
|
||||
a.inboundController = NewInboundController(g)
|
||||
a.settingController = NewSettingController(g)
|
||||
a.xraySettingController = NewXraySettingController(g)
|
||||
}
|
||||
|
||||
// index renders the main panel index page.
|
||||
func (a *XUIController) index(c *gin.Context) {
|
||||
html(c, "index.html", "pages.index.title", nil)
|
||||
}
|
||||
|
||||
// inbounds renders the inbounds management page.
|
||||
func (a *XUIController) inbounds(c *gin.Context) {
|
||||
html(c, "inbounds.html", "pages.inbounds.title", nil)
|
||||
}
|
||||
|
||||
// settings renders the settings management page.
|
||||
func (a *XUIController) settings(c *gin.Context) {
|
||||
html(c, "settings.html", "pages.settings.title", nil)
|
||||
}
|
||||
|
||||
// xraySettings renders the Xray settings page.
|
||||
func (a *XUIController) xraySettings(c *gin.Context) {
|
||||
html(c, "xray.html", "pages.xray.title", nil)
|
||||
}
|
||||
|
||||
@@ -1,69 +1,107 @@
|
||||
// Package entity defines data structures and entities used by the web layer of the 3x-ui panel.
|
||||
package entity
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"math"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
"math"
|
||||
|
||||
"x-ui/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||
)
|
||||
|
||||
// Msg represents a standard API response message with success status, message text, and optional data object.
|
||||
type Msg struct {
|
||||
Success bool `json:"success"`
|
||||
Msg string `json:"msg"`
|
||||
Obj any `json:"obj"`
|
||||
Success bool `json:"success"` // Indicates if the operation was successful
|
||||
Msg string `json:"msg"` // Response message text
|
||||
Obj any `json:"obj"` // Optional data object
|
||||
}
|
||||
|
||||
// AllSetting contains all configuration settings for the 3x-ui panel including web server, Telegram bot, and subscription settings.
|
||||
type AllSetting struct {
|
||||
WebListen string `json:"webListen" form:"webListen"`
|
||||
WebDomain string `json:"webDomain" form:"webDomain"`
|
||||
WebPort int `json:"webPort" form:"webPort"`
|
||||
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
||||
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
||||
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
|
||||
PageSize int `json:"pageSize" form:"pageSize"`
|
||||
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
|
||||
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
|
||||
RemarkModel string `json:"remarkModel" form:"remarkModel"`
|
||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
||||
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"`
|
||||
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"`
|
||||
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
|
||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
||||
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
|
||||
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
|
||||
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
||||
TgLang string `json:"tgLang" form:"tgLang"`
|
||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
||||
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
||||
SubTitle string `json:"subTitle" form:"subTitle"`
|
||||
SubListen string `json:"subListen" form:"subListen"`
|
||||
SubPort int `json:"subPort" form:"subPort"`
|
||||
SubPath string `json:"subPath" form:"subPath"`
|
||||
SubDomain string `json:"subDomain" form:"subDomain"`
|
||||
SubCertFile string `json:"subCertFile" form:"subCertFile"`
|
||||
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
|
||||
SubUpdates int `json:"subUpdates" form:"subUpdates"`
|
||||
ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"`
|
||||
ExternalTrafficInformURI string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"`
|
||||
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
|
||||
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
|
||||
SubURI string `json:"subURI" form:"subURI"`
|
||||
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
|
||||
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
|
||||
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
|
||||
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"`
|
||||
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
|
||||
// Web server settings
|
||||
WebListen string `json:"webListen" form:"webListen"` // Web server listen IP address
|
||||
WebDomain string `json:"webDomain" form:"webDomain"` // Web server domain for domain validation
|
||||
WebPort int `json:"webPort" form:"webPort"` // Web server port number
|
||||
WebCertFile string `json:"webCertFile" form:"webCertFile"` // Path to SSL certificate file for web server
|
||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` // Path to SSL private key file for web server
|
||||
WebBasePath string `json:"webBasePath" form:"webBasePath"` // Base path for web panel URLs
|
||||
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"` // Session maximum age in minutes
|
||||
|
||||
// UI settings
|
||||
PageSize int `json:"pageSize" form:"pageSize"` // Number of items per page in lists
|
||||
ExpireDiff int `json:"expireDiff" form:"expireDiff"` // Expiration warning threshold in days
|
||||
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"` // Traffic warning threshold percentage
|
||||
RemarkModel string `json:"remarkModel" form:"remarkModel"` // Remark model pattern for inbounds
|
||||
Datepicker string `json:"datepicker" form:"datepicker"` // Date picker format
|
||||
|
||||
// Telegram bot settings
|
||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` // Enable Telegram bot notifications
|
||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"` // Telegram bot token
|
||||
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"` // Proxy URL for Telegram bot
|
||||
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"` // Custom API server for Telegram bot
|
||||
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"` // Telegram chat ID for notifications
|
||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"` // Cron schedule for Telegram notifications
|
||||
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` // Enable database backup via Telegram
|
||||
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"` // Send login notifications
|
||||
TgCpu int `json:"tgCpu" form:"tgCpu"` // CPU usage threshold for alerts
|
||||
TgLang string `json:"tgLang" form:"tgLang"` // Telegram bot language
|
||||
|
||||
// Security settings
|
||||
TimeLocation string `json:"timeLocation" form:"timeLocation"` // Time zone location
|
||||
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"` // Enable two-factor authentication
|
||||
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"` // Two-factor authentication token
|
||||
|
||||
// Subscription server settings
|
||||
SubEnable bool `json:"subEnable" form:"subEnable"` // Enable subscription server
|
||||
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"` // Enable JSON subscription endpoint
|
||||
SubTitle string `json:"subTitle" form:"subTitle"` // Subscription title
|
||||
SubListen string `json:"subListen" form:"subListen"` // Subscription server listen IP
|
||||
SubPort int `json:"subPort" form:"subPort"` // Subscription server port
|
||||
SubPath string `json:"subPath" form:"subPath"` // Base path for subscription URLs
|
||||
SubDomain string `json:"subDomain" form:"subDomain"` // Domain for subscription server validation
|
||||
SubCertFile string `json:"subCertFile" form:"subCertFile"` // SSL certificate file for subscription server
|
||||
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"` // SSL private key file for subscription server
|
||||
SubUpdates int `json:"subUpdates" form:"subUpdates"` // Subscription update interval in minutes
|
||||
ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"` // Enable external traffic reporting
|
||||
ExternalTrafficInformURI string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"` // URI for external traffic reporting
|
||||
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"` // Encrypt subscription responses
|
||||
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"` // Show client information in subscriptions
|
||||
SubURI string `json:"subURI" form:"subURI"` // Subscription server URI
|
||||
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"` // Path for JSON subscription endpoint
|
||||
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"` // JSON subscription server URI
|
||||
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"` // JSON subscription fragment configuration
|
||||
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"` // JSON subscription noise configuration
|
||||
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` // JSON subscription mux configuration
|
||||
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
|
||||
Datepicker string `json:"datepicker" form:"datepicker"`
|
||||
|
||||
// LDAP settings
|
||||
LdapEnable bool `json:"ldapEnable" form:"ldapEnable"`
|
||||
LdapHost string `json:"ldapHost" form:"ldapHost"`
|
||||
LdapPort int `json:"ldapPort" form:"ldapPort"`
|
||||
LdapUseTLS bool `json:"ldapUseTLS" form:"ldapUseTLS"`
|
||||
LdapBindDN string `json:"ldapBindDN" form:"ldapBindDN"`
|
||||
LdapPassword string `json:"ldapPassword" form:"ldapPassword"`
|
||||
LdapBaseDN string `json:"ldapBaseDN" form:"ldapBaseDN"`
|
||||
LdapUserFilter string `json:"ldapUserFilter" form:"ldapUserFilter"`
|
||||
LdapUserAttr string `json:"ldapUserAttr" form:"ldapUserAttr"` // e.g., mail or uid
|
||||
LdapVlessField string `json:"ldapVlessField" form:"ldapVlessField"`
|
||||
LdapSyncCron string `json:"ldapSyncCron" form:"ldapSyncCron"`
|
||||
// Generic flag configuration
|
||||
LdapFlagField string `json:"ldapFlagField" form:"ldapFlagField"`
|
||||
LdapTruthyValues string `json:"ldapTruthyValues" form:"ldapTruthyValues"`
|
||||
LdapInvertFlag bool `json:"ldapInvertFlag" form:"ldapInvertFlag"`
|
||||
LdapInboundTags string `json:"ldapInboundTags" form:"ldapInboundTags"`
|
||||
LdapAutoCreate bool `json:"ldapAutoCreate" form:"ldapAutoCreate"`
|
||||
LdapAutoDelete bool `json:"ldapAutoDelete" form:"ldapAutoDelete"`
|
||||
LdapDefaultTotalGB int `json:"ldapDefaultTotalGB" form:"ldapDefaultTotalGB"`
|
||||
LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays"`
|
||||
LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP"`
|
||||
// JSON subscription routing rules
|
||||
}
|
||||
|
||||
// CheckValid validates all settings in the AllSetting struct, checking IP addresses, ports, SSL certificates, and other configuration values.
|
||||
func (s *AllSetting) CheckValid() error {
|
||||
if s.WebListen != "" {
|
||||
ip := net.ParseIP(s.WebListen)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package global provides global variables and interfaces for accessing web and subscription servers.
|
||||
package global
|
||||
|
||||
import (
|
||||
@@ -12,27 +13,34 @@ var (
|
||||
subServer SubServer
|
||||
)
|
||||
|
||||
// WebServer interface defines methods for accessing the web server instance.
|
||||
type WebServer interface {
|
||||
GetCron() *cron.Cron
|
||||
GetCtx() context.Context
|
||||
GetCron() *cron.Cron // Get the cron scheduler
|
||||
GetCtx() context.Context // Get the server context
|
||||
GetWSHub() interface{} // Get the WebSocket hub (using interface{} to avoid circular dependency)
|
||||
}
|
||||
|
||||
// SubServer interface defines methods for accessing the subscription server instance.
|
||||
type SubServer interface {
|
||||
GetCtx() context.Context
|
||||
GetCtx() context.Context // Get the server context
|
||||
}
|
||||
|
||||
// SetWebServer sets the global web server instance.
|
||||
func SetWebServer(s WebServer) {
|
||||
webServer = s
|
||||
}
|
||||
|
||||
// GetWebServer returns the global web server instance.
|
||||
func GetWebServer() WebServer {
|
||||
return webServer
|
||||
}
|
||||
|
||||
// SetSubServer sets the global subscription server instance.
|
||||
func SetSubServer(s SubServer) {
|
||||
subServer = s
|
||||
}
|
||||
|
||||
// GetSubServer returns the global subscription server instance.
|
||||
func GetSubServer() SubServer {
|
||||
return subServer
|
||||
}
|
||||
|
||||
@@ -8,18 +8,21 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// HashEntry represents a stored hash entry with its value and timestamp.
|
||||
type HashEntry struct {
|
||||
Hash string
|
||||
Value string
|
||||
Timestamp time.Time
|
||||
Hash string // MD5 hash string
|
||||
Value string // Original value
|
||||
Timestamp time.Time // Time when the hash was created
|
||||
}
|
||||
|
||||
// HashStorage provides thread-safe storage for hash-value pairs with expiration.
|
||||
type HashStorage struct {
|
||||
sync.RWMutex
|
||||
Data map[string]HashEntry
|
||||
Expiration time.Duration
|
||||
Data map[string]HashEntry // Map of hash to entry
|
||||
Expiration time.Duration // Expiration duration for entries
|
||||
}
|
||||
|
||||
// NewHashStorage creates a new HashStorage instance with the specified expiration duration.
|
||||
func NewHashStorage(expiration time.Duration) *HashStorage {
|
||||
return &HashStorage{
|
||||
Data: make(map[string]HashEntry),
|
||||
@@ -27,6 +30,7 @@ func NewHashStorage(expiration time.Duration) *HashStorage {
|
||||
}
|
||||
}
|
||||
|
||||
// SaveHash generates an MD5 hash for the given query string and stores it with a timestamp.
|
||||
func (h *HashStorage) SaveHash(query string) string {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
@@ -45,6 +49,7 @@ func (h *HashStorage) SaveHash(query string) string {
|
||||
return md5HashString
|
||||
}
|
||||
|
||||
// GetValue retrieves the original value for the given hash, returning true if found.
|
||||
func (h *HashStorage) GetValue(hash string) (string, bool) {
|
||||
h.RLock()
|
||||
defer h.RUnlock()
|
||||
@@ -54,11 +59,13 @@ func (h *HashStorage) GetValue(hash string) (string, bool) {
|
||||
return entry.Value, exists
|
||||
}
|
||||
|
||||
// IsMD5 checks if the given string is a valid 32-character MD5 hash.
|
||||
func (h *HashStorage) IsMD5(hash string) bool {
|
||||
match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash)
|
||||
return match
|
||||
}
|
||||
|
||||
// RemoveExpiredHashes removes all hash entries that have exceeded the expiration duration.
|
||||
func (h *HashStorage) RemoveExpiredHashes() {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
@@ -72,6 +79,7 @@ func (h *HashStorage) RemoveExpiredHashes() {
|
||||
}
|
||||
}
|
||||
|
||||
// Reset clears all stored hash entries.
|
||||
func (h *HashStorage) Reset() {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
@@ -44,12 +44,12 @@
|
||||
<script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/qs/qs.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
|
||||
<script>
|
||||
const basePath = '{{ .base_path }}';
|
||||
axios.defaults.baseURL = basePath;
|
||||
</script>
|
||||
<script src="{{ .base_path }}assets/js/websocket.js?{{ .cur_ver }}"></script>
|
||||
{{ end }}
|
||||
|
||||
{{ define "page/body_end" }}
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
<template slot="actions" slot-scope="text, client, index">
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "qrCode" }}</template>
|
||||
<a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
|
||||
<a-icon :style="{ fontSize: '22px', marginInlineStart: '14px' }" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
||||
<a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
|
||||
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "info" }}</template>
|
||||
<a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
|
||||
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" :style="{ color: 'var(--color-primary-100)'}"></a-icon>
|
||||
<a-icon :style="{ fontSize: '24px', cursor: 'pointer' }" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
|
||||
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
@@ -25,7 +25,7 @@
|
||||
</template>
|
||||
<a-popconfirm @confirm="delClient(record.id,client,false)" title='{{ i18n "pages.inbounds.deleteClientContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "delete"}}' ok-type="danger" cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" :style="{ color: '#e04141' }"></a-icon>
|
||||
<a-icon :style="{ fontSize: '24px', cursor: 'pointer' }" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
|
||||
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
@@ -33,18 +33,23 @@
|
||||
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
|
||||
</template>
|
||||
<template slot="online" slot-scope="text, client, index">
|
||||
<template v-if="client.enable && isClientOnline(client.email)">
|
||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>{{ i18n "offline" }}</a-tag>
|
||||
</template>
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content" >
|
||||
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
|
||||
</template>
|
||||
<template v-if="client.enable && isClientOnline(client.email)">
|
||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>{{ i18n "offline" }}</a-tag>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="client" slot-scope="text, client">
|
||||
<a-space direction="horizontal" :size="2">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
|
||||
<template v-if="isClientDepleted(record, client.email)">{{ i18n "depleted" }}</template>
|
||||
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
|
||||
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
|
||||
</template>
|
||||
@@ -85,7 +90,7 @@
|
||||
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
|
||||
</td>
|
||||
<td class="tr-table-bar" v-else-if="client.totalGB > 0">
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||
</td>
|
||||
<td v-else class="infinite-bar tr-table-bar">
|
||||
<a-progress :show-info="false" :percent="100"></a-progress>
|
||||
@@ -98,26 +103,22 @@
|
||||
</table>
|
||||
</a-popover>
|
||||
</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 v-if="client.expiryTime !=0 && client.reset >0">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
|
||||
<span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
|
||||
</template>
|
||||
<table>
|
||||
<tr class="tr-table-box">
|
||||
<td class="tr-table-rt"> [[ remainedDays(client.expiryTime) ]] </td>
|
||||
<td class="tr-table-rt"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </td>
|
||||
<td class="infinite-bar tr-table-bar">
|
||||
<a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||
</td>
|
||||
<td class="tr-table-lt">[[ client.reset + "d" ]]</td>
|
||||
</tr>
|
||||
@@ -127,18 +128,10 @@
|
||||
<template v-else>
|
||||
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
|
||||
<span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
|
||||
</template>
|
||||
<a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag>
|
||||
<a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" :style="{ border: 'none' }" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
@@ -204,7 +197,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||
</a-popover>
|
||||
</td>
|
||||
<td width="120px" v-else class="infinite-bar">
|
||||
@@ -223,22 +216,14 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<template v-if="client.expiryTime !=0 && client.reset >0">
|
||||
<td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[ remainedDays(client.expiryTime) ]] </td>
|
||||
<td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </td>
|
||||
<td width="120px" class="infinite-bar">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
|
||||
<span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
|
||||
</template>
|
||||
<a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||
</a-popover>
|
||||
</td>
|
||||
<td width="60px">[[ client.reset + "d" ]]</td>
|
||||
@@ -247,18 +232,10 @@
|
||||
<td colspan="3" :style="{ textAlign: 'center' }">
|
||||
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
|
||||
<span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
|
||||
</template>
|
||||
<a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag>
|
||||
<a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
@@ -278,4 +255,20 @@
|
||||
</a-badge>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="createdAt" slot-scope="text, client, index">
|
||||
<template v-if="client.created_at">
|
||||
[[ IntlUtil.formatDate(client.created_at) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</template>
|
||||
<template slot="updatedAt" slot-scope="text, client, index">
|
||||
<template v-if="client.updated_at">
|
||||
[[ IntlUtil.formatDate(client.updated_at) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
@@ -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}}
|
||||
@@ -44,7 +44,7 @@
|
||||
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</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">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number v-model.number="inbound.port" :min="1" :max="65531"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.port" :min="1" :max="65535"></a-input-number>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
@@ -44,6 +44,29 @@
|
||||
<a-input-number v-model.number="dbInbound.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.periodicTrafficResetDesc" }}</span>
|
||||
<br v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
|
||||
<span v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
|
||||
<strong>{{ i18n "pages.inbounds.lastReset" }}:</strong>
|
||||
<span>[[ IntlUtil.formatDate(dbInbound.lastTrafficResetTime) ]]</span>
|
||||
</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.periodicTrafficResetTitle" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-select v-model="dbInbound.trafficReset" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="never">{{ i18n "pages.inbounds.periodicTrafficReset.never" }}</a-select-option>
|
||||
<a-select-option value="daily">{{ i18n "pages.inbounds.periodicTrafficReset.daily" }}</a-select-option>
|
||||
<a-select-option value="weekly">{{ i18n "pages.inbounds.periodicTrafficReset.weekly" }}</a-select-option>
|
||||
<a-select-option value="monthly">{{ i18n "pages.inbounds.periodicTrafficReset.monthly" }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
@@ -83,14 +106,14 @@
|
||||
{{template "form/shadowsocks"}}
|
||||
</template>
|
||||
|
||||
<!-- dokodemo-door -->
|
||||
<template v-if="inbound.protocol === Protocols.DOKODEMO">
|
||||
{{template "form/dokodemo"}}
|
||||
<!-- tunnel -->
|
||||
<template v-if="inbound.protocol === Protocols.TUNNEL">
|
||||
{{template "form/tunnel"}}
|
||||
</template>
|
||||
|
||||
<!-- socks -->
|
||||
<template v-if="inbound.protocol === Protocols.SOCKS">
|
||||
{{template "form/socks"}}
|
||||
<!-- mixed -->
|
||||
<template v-if="inbound.protocol === Protocols.MIXED">
|
||||
{{template "form/mixed"}}
|
||||
</template>
|
||||
|
||||
<!-- http -->
|
||||
@@ -121,13 +144,4 @@
|
||||
</a-collapse-panel>
|
||||
</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}}
|
||||
@@ -1,6 +1,7 @@
|
||||
{{define "form/outbound"}}
|
||||
<!-- base -->
|
||||
<a-tabs :active-key="outModal.activeKey" :style="{ padding: '0', backgroundColor: 'transparent' }" @change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }">
|
||||
<a-tabs :active-key="outModal.activeKey" :style="{ padding: '0', backgroundColor: 'transparent' }"
|
||||
@change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }">
|
||||
<a-tab-pane key="1" tab="Form">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
@@ -8,8 +9,10 @@
|
||||
<a-select-option v-for="x,y in Protocols" :value="x">[[ y ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback :validate-status="outModal.duplicateTag? 'warning' : 'success'">
|
||||
<a-input v-model.trim="outbound.tag" @change="outModal.check()" placeholder='{{ i18n "pages.xray.outbound.tagDesc" }}'></a-input>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback
|
||||
:validate-status="outModal.duplicateTag? 'warning' : 'success'">
|
||||
<a-input v-model.trim="outbound.tag" @change="outModal.check()"
|
||||
placeholder='{{ i18n "pages.xray.outbound.tagDesc" }}'></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.sendThrough" }}'>
|
||||
<a-input v-model="outbound.sendThrough"></a-input>
|
||||
@@ -42,6 +45,9 @@
|
||||
<a-form-item label='Interval'>
|
||||
<a-input v-model.trim="outbound.settings.fragment.interval"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Split'>
|
||||
<a-input v-model.trim="outbound.settings.fragment.maxSplit"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- Switch for Noises -->
|
||||
@@ -56,12 +62,13 @@
|
||||
<a-form-item label="Noises">
|
||||
<a-button icon="plus" type="primary" size="small" @click="outbound.settings.addNoise()"></a-button>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Noise Configurations -->
|
||||
<a-form v-for="(noise, index) in outbound.settings.noises" :key="index" :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
|
||||
<!-- Noise Configurations -->
|
||||
<a-form v-for="(noise, index) in outbound.settings.noises" :key="index" :colon="false"
|
||||
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> Noise [[ index + 1 ]]
|
||||
<a-icon v-if="outbound.settings.noises.length > 1" type="delete" @click="() => outbound.settings.delNoise(index)"
|
||||
<a-icon v-if="outbound.settings.noises.length > 1" type="delete"
|
||||
@click="() => outbound.settings.delNoise(index)"
|
||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='Type'>
|
||||
@@ -75,6 +82,11 @@
|
||||
<a-form-item label='Delay'>
|
||||
<a-input v-model.trim="noise.delay"></a-input>
|
||||
</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>
|
||||
</template>
|
||||
</template>
|
||||
@@ -97,10 +109,10 @@
|
||||
</a-form-item>
|
||||
<a-form-item label='non-IP queries'>
|
||||
<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-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'>
|
||||
<a-input v-model.number="outbound.settings.blockTypes"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
@@ -121,21 +133,21 @@
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.wireguard.secretKey" }}
|
||||
<a-icon type="sync"
|
||||
@click="[outbound.settings.pubKey, outbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())">
|
||||
</a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.wireguard.secretKey" }}
|
||||
<a-icon type="sync"
|
||||
@click="[outbound.settings.pubKey, outbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())">
|
||||
</a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="outbound.settings.secretKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
|
||||
<a-input disabled v-model="outbound.settings.pubKey"></a-input>
|
||||
</a-form-item>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.domainStrategy" }}'>
|
||||
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="wds in ['', ...WireguardDomainStrategy]" :value="wds">[[ wds ]]</a-select-option>
|
||||
@@ -163,8 +175,11 @@
|
||||
<a-form-item label="Peers">
|
||||
<a-button icon="plus" type="primary" size="small" @click="outbound.settings.addPeer()"></a-button>
|
||||
</a-form-item>
|
||||
<a-form v-for="(peer, index) in outbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> Peer [[ index + 1 ]] <a-icon v-if="outbound.settings.peers.length>1" type="delete" @click="() => outbound.settings.delPeer(index)" :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
<a-form v-for="(peer, index) in outbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> Peer [[ index + 1 ]] <a-icon v-if="outbound.settings.peers.length>1"
|
||||
type="delete" @click="() => outbound.settings.delPeer(index)"
|
||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.endpoint" }}'>
|
||||
<a-input v-model.trim="peer.endpoint"></a-input>
|
||||
@@ -182,7 +197,8 @@
|
||||
</template>
|
||||
<template v-for="(aip, index) in peer.allowedIPs" :style="{ marginBottom: '10px' }">
|
||||
<a-input v-model.trim="peer.allowedIPs[index]">
|
||||
<a-button icon="minus" v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)"></a-button>
|
||||
<a-button icon="minus" v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small"
|
||||
@click="peer.allowedIPs.splice(index, 1)"></a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
@@ -202,7 +218,7 @@
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- Vnext (vless/vmess) settings -->
|
||||
<!-- VLESS/VMess user settings -->
|
||||
<template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
|
||||
<a-form-item label='ID'>
|
||||
<a-input v-model.trim="outbound.settings.id"></a-input>
|
||||
@@ -218,6 +234,11 @@
|
||||
</template>
|
||||
|
||||
<!-- 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()">
|
||||
<a-form-item label='Flow'>
|
||||
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
@@ -226,6 +247,33 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<!-- XTLS Vision Advanced Settings -->
|
||||
<template v-if="outbound.canEnableVisionSeed()">
|
||||
<a-form-item label="Vision Pre-Connect">
|
||||
<a-input-number v-model.number="outbound.settings.testpre" :min="0" :max="10" :style="{ width: '100%' }"
|
||||
placeholder="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="Vision Seed">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[0]" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="900" addon-before="[0]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[1]" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="500" addon-before="[1]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[2]" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="900" addon-before="[2]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number v-model.number="outbound.settings.testseed[3]" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }" placeholder="256" addon-before="[3]"></a-input-number>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Servers (trojan/shadowsocks/socks/http) settings -->
|
||||
@@ -251,7 +299,8 @@
|
||||
<template v-if="outbound.protocol === Protocols.Shadowsocks">
|
||||
<a-form-item label='{{ i18n "encryption" }}'>
|
||||
<a-select v-model="outbound.settings.method" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="(method, method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option>
|
||||
<a-select-option v-for="(method, method_name) in SSMethods" :value="method">[[ method_name
|
||||
]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='UDP over TCP'>
|
||||
@@ -266,7 +315,8 @@
|
||||
<!-- stream settings -->
|
||||
<template v-if="outbound.canEnableStream()">
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="outbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select v-model="outbound.stream.network" @change="streamNetworkChange"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp">TCP (RAW)</a-select-option>
|
||||
<a-select-option value="kcp">mKCP</a-select-option>
|
||||
<a-select-option value="ws">WebSocket</a-select-option>
|
||||
@@ -277,7 +327,8 @@
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.network === 'tcp'">
|
||||
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
|
||||
<a-switch :checked="outbound.stream.tcp.type === 'http'" @change="checked => outbound.stream.tcp.type = checked ? 'http' : 'none'"></a-switch>
|
||||
<a-switch :checked="outbound.stream.tcp.type === 'http'"
|
||||
@change="checked => outbound.stream.tcp.type = checked ? 'http' : 'none'"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.tcp.type == 'http'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
@@ -340,7 +391,7 @@
|
||||
<a-input-number v-model.number="outbound.stream.ws.heartbeatPeriod" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- grpc -->
|
||||
<template v-if="outbound.stream.network === 'grpc'">
|
||||
<a-form-item label='Service Name'>
|
||||
@@ -377,7 +428,8 @@
|
||||
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="No gRPC Header" v-if="outbound.stream.xhttp.mode === 'stream-up' || outbound.stream.xhttp.mode === 'stream-one'">
|
||||
<a-form-item label="No gRPC Header"
|
||||
v-if="outbound.stream.xhttp.mode === 'stream-up' || outbound.stream.xhttp.mode === 'stream-one'">
|
||||
<a-switch v-model="outbound.stream.xhttp.noGRPCHeader"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Min Upload Interval (Ms)" v-if="outbound.stream.xhttp.mode === 'packet-up'">
|
||||
@@ -424,10 +476,14 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="ALPN">
|
||||
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="outbound.stream.tls.alpn">
|
||||
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="outbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item 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-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
@@ -444,13 +500,16 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<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 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 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>
|
||||
</template>
|
||||
</template>
|
||||
@@ -466,7 +525,8 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Address Port Strategy'>
|
||||
<a-select v-model="outbound.stream.sockopt.addressPortStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select v-model="outbound.stream.sockopt.addressPortStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in Address_Port_Strategy" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
@@ -482,6 +542,15 @@
|
||||
<a-form-item label="Penetrate">
|
||||
<a-switch v-model="outbound.stream.sockopt.penetrate"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Trusted X-Forwarded-For">
|
||||
<a-select mode="tags" v-model="outbound.stream.sockopt.trustedXForwardedFor" :style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
|
||||
<a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
|
||||
<a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
|
||||
<a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- mux settings -->
|
||||
@@ -507,11 +576,12 @@
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="JSON" force-render="true">
|
||||
<a-space direction="vertical" :size="10" :style="{ marginTop: '10px' }">
|
||||
<a-input addon-before='{{ i18n "pages.xray.outbound.link" }}' v-model.trim="outModal.link" placeholder="vmess:// vless:// trojan:// ss://">
|
||||
<a-input addon-before='{{ i18n "pages.xray.outbound.link" }}' v-model.trim="outModal.link"
|
||||
placeholder="vmess:// vless:// trojan:// ss://">
|
||||
<a-icon slot="addonAfter" type="form" @click="convertLink"></a-icon>
|
||||
</a-input>
|
||||
<textarea :style="{ position: 'absolute', left: '-800px' }" id="outboundJson"></textarea>
|
||||
</a-space>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -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-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
|
||||
<a-input v-model.trim="inbound.settings.address"></a-input>
|
||||
@@ -6,6 +6,19 @@
|
||||
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
|
||||
<a-input-number v-model.number="inbound.settings.port"></a-input-number>
|
||||
</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-select v-model="inbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<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-form-item>
|
||||
</a-form>
|
||||
<!-- sockopt -->
|
||||
<template>
|
||||
{{template "form/streamSockopt"}}
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
@@ -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-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
|
||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||
@@ -15,7 +15,7 @@
|
||||
<td width="45%">{{ i18n "username" }}</td>
|
||||
<td width="45%">{{ i18n "password" }}</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>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -1,33 +1,72 @@
|
||||
{{define "form/vless"}}
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse activeKey="0"
|
||||
v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
{{template "form/client"}}
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<a-collapse v-else>
|
||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
|
||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' +
|
||||
inbound.settings.vlesses.length">
|
||||
<table width="100%">
|
||||
<tr class="client-table-header">
|
||||
<th>{{ i18n "pages.inbounds.email" }}</th>
|
||||
<th>ID</th>
|
||||
</tr>
|
||||
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||
<tr v-for="(client, index) in inbound.settings.vlesses"
|
||||
:class="index % 2 == 1 ? ' client-table-odd-row' : ''">
|
||||
<td>[[ client.email ]]</td>
|
||||
<td>[[ client.id ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<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>
|
||||
<a-divider v-if="inbound.settings.selectedAuth"
|
||||
:style="{ margin: '5px 0' }"></a-divider>
|
||||
</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-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>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vless fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete" @click="() => inbound.settings.delFallback(index)" :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false"
|
||||
:label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon
|
||||
type="delete"
|
||||
@click="() => inbound.settings.delFallback(index)"
|
||||
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
@@ -42,9 +81,56 @@
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='xVer'>
|
||||
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
|
||||
<a-input-number v-model.number="fallback.xver" :min="0"
|
||||
:max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||
</template>
|
||||
{{end}}
|
||||
<template v-if="inbound.canEnableVisionSeed()">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Vision Seed">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="6">
|
||||
<a-input-number
|
||||
:value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900"
|
||||
@change="(val) => updateTestseed(0, val)" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }"
|
||||
placeholder="900" addon-before="[0]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number
|
||||
:value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500"
|
||||
@change="(val) => updateTestseed(1, val)" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }"
|
||||
placeholder="500" addon-before="[1]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number
|
||||
:value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900"
|
||||
@change="(val) => updateTestseed(2, val)" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }"
|
||||
placeholder="900" addon-before="[2]"></a-input-number>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input-number
|
||||
:value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256"
|
||||
@change="(val) => updateTestseed(3, val)" :min="0" :max="9999"
|
||||
:style="{ width: '100%' }"
|
||||
placeholder="256" addon-before="[3]"></a-input-number>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-space :size="8" :style="{ marginTop: '8px' }">
|
||||
<a-button type="primary" @click="setRandomTestseed">
|
||||
Rand
|
||||
</a-button>
|
||||
<a-button @click="resetTestseed">
|
||||
Reset
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||
</template>
|
||||
{{end}}
|
||||
@@ -12,23 +12,37 @@
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Dest (Target)'>
|
||||
<a-input v-model.trim="inbound.stream.reality.dest"></a-input>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template> Target <a-icon @click="randomizeRealityTarget()"
|
||||
type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.stream.reality.target"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='SNI'>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template> SNI <a-icon @click="randomizeRealityTarget()"
|
||||
type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Time Diff (ms)'>
|
||||
<a-input-number v-model.number="inbound.stream.reality.maxTimediff" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<!-- we also have this but i think it's not necessary
|
||||
<a-form-item label='Min Client'>
|
||||
<a-input v-model.trim="inbound.stream.reality.minClient"></a-input>
|
||||
<a-form-item label='Min Client Ver'>
|
||||
<a-input v-model.trim="inbound.stream.reality.minClientVer" placeholder='25.9.11'></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Client'>
|
||||
<a-input v-model.trim="inbound.stream.reality.maxClient"></a-input>
|
||||
<a-form-item label='Max Client Ver'>
|
||||
<a-input v-model.trim="inbound.stream.reality.maxClientVer" placeholder='25.9.11'></a-input>
|
||||
</a-form-item>
|
||||
-->
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
@@ -38,19 +52,34 @@
|
||||
type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</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 label='SpiderX'>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.spiderX"></a-input>
|
||||
</a-form-item>
|
||||
<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 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 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>
|
||||
</template>
|
||||
{{end}}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user