Compare commits

..

No commits in common. "main" and "v2.6.7" have entirely different histories.
main ... v2.6.7

147 changed files with 3486 additions and 8518 deletions

2
.github/FUNDING.yml vendored
View file

@ -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 lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username polar: # Replace with a single Polar username
buy_me_a_coffee: mhsanaei buy_me_a_coffee: mhsanaei
custom: https://nowpayments.io/donation/hsanaei custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View file

@ -1,9 +1,4 @@
name: Release 3X-UI for Docker name: Release 3X-UI for Docker
permissions:
contents: read
packages: write
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
@ -15,48 +10,48 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
submodules: true submodules: true
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: | images: |
hsanaeii/3x-ui hsanaeii/3x-ui
ghcr.io/mhsanaei/3x-ui ghcr.io/mhsanaei/3x-ui
tags: | tags: |
type=ref,event=branch type=ref,event=branch
type=ref,event=tag type=ref,event=tag
type=semver,pattern={{version}} type=pep440,pattern={{version}}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
with: with:
install: true install: true
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }} password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386 platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View file

@ -7,9 +7,8 @@ on:
push: push:
branches: branches:
- main - main
tags:
- "v*.*.*"
paths: paths:
- '.github/workflows/release.yml'
- '**.js' - '**.js'
- '**.css' - '**.css'
- '**.html' - '**.html'
@ -39,7 +38,7 @@ jobs:
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@ -85,7 +84,7 @@ jobs:
cd x-ui/bin cd x-ui/bin
# Download dependencies # Download dependencies
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.9.11/" Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.8.29/"
if [ "${{ matrix.platform }}" == "amd64" ]; then if [ "${{ matrix.platform }}" == "amd64" ]; then
wget -q ${Xray_URL}Xray-linux-64.zip wget -q ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip unzip Xray-linux-64.zip
@ -136,89 +135,10 @@ jobs:
- name: Upload files to GH release - name: Upload files to GH release
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2
if: | if: github.event_name == 'release' && github.event.action == 'published'
(github.event_name == 'release' && github.event.action == 'published') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }} tag: ${{ github.ref }}
file: x-ui-linux-${{ matrix.platform }}.tar.gz file: x-ui-linux-${{ matrix.platform }}.tar.gz
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
overwrite: true
prerelease: true
# =================================
# Windows Build
# =================================
build-windows:
name: Build for Windows
permissions:
contents: write
strategy:
matrix:
platform:
- amd64
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Build 3X-UI for Windows
shell: pwsh
run: |
$env:CGO_ENABLED="1"
$env:GOOS="windows"
$env:GOARCH="amd64"
go build -ldflags "-w -s" -o xui-release.exe -v main.go
mkdir x-ui
Copy-Item xui-release.exe x-ui\
mkdir x-ui\bin
cd x-ui\bin
# Download Xray for Windows
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.9.11/"
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
Remove-Item "Xray-windows-64.zip"
Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat"
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat"
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat"
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat"
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat"
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat"
Rename-Item xray.exe xray-windows-amd64.exe
cd ..
Copy-Item -Path ..\windows_files\* -Destination . -Recurse
cd ..
- name: Package to Zip
shell: pwsh
run: |
Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
- name: Upload files to Artifacts
uses: actions/upload-artifact@v4
with:
name: x-ui-windows-amd64
path: ./x-ui-windows-amd64.zip
- name: Upload files to GH release
uses: svenstaro/upload-release-action@v2
if: |
(github.event_name == 'release' && github.event.action == 'published') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-windows-amd64.zip
asset_name: x-ui-windows-amd64.zip
overwrite: true
prerelease: true prerelease: true

35
.vscode/launch.json vendored
View file

@ -1,35 +0,0 @@
{
"$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
View file

@ -1,75 +0,0 @@
{
"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"
]
}
]
}

View file

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

View file

@ -49,7 +49,6 @@ RUN chmod +x \
/usr/bin/x-ui /usr/bin/x-ui
ENV XUI_ENABLE_FAIL2BAN="true" ENV XUI_ENABLE_FAIL2BAN="true"
EXPOSE 2053
VOLUME [ "/etc/x-ui" ] VOLUME [ "/etc/x-ui" ]
CMD [ "./x-ui" ] CMD [ "./x-ui" ]
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ] ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]

View file

@ -7,13 +7,11 @@
</picture> </picture>
</p> </p>
[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) [![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions) [![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#) [![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest) [![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
**3X-UI** — لوحة تحكم متقدمة مفتوحة المصدر تعتمد على الويب مصممة لإدارة خادم Xray-core. توفر واجهة سهلة الاستخدام لتكوين ومراقبة بروتوكولات VPN والوكيل المختلفة. **3X-UI** — لوحة تحكم متقدمة مفتوحة المصدر تعتمد على الويب مصممة لإدارة خادم Xray-core. توفر واجهة سهلة الاستخدام لتكوين ومراقبة بروتوكولات VPN والوكيل المختلفة.
@ -43,13 +41,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
**إذا كان هذا المشروع مفيدًا لك، فقد ترغب في إعطائه**:star2: **إذا كان هذا المشروع مفيدًا لك، فقد ترغب في إعطائه**:star2:
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank"> <p align="left">
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" > <a href="https://buymeacoffee.com/mhsanaei" target="_blank">
</a> <img src="./media/buymeacoffe.png" alt="Image">
</br> </a>
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener"> </p>
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
</a> - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## النجوم عبر الزمن ## النجوم عبر الزمن

View file

@ -7,13 +7,11 @@
</picture> </picture>
</p> </p>
[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) [![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions) [![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#) [![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest) [![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
[![Go Report Card](https://goreportcard.com/badge/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. **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.
@ -43,14 +41,15 @@ Para documentación completa, visita la [Wiki del proyecto](https://github.com/M
**Si este proyecto te es útil, puedes darle una**:star2: **Si este proyecto te es útil, puedes darle una**:star2:
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank"> <p align="left">
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" > <a href="https://buymeacoffee.com/mhsanaei" target="_blank">
</a> <img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
</br> - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener"> - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments"> - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
</a>
## Estrellas a lo Largo del Tiempo ## Estrellas a lo Largo del Tiempo

View file

@ -7,13 +7,11 @@
</picture> </picture>
</p> </p>
[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) [![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions) [![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#) [![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest) [![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
**3X-UI** — یک پنل کنترل پیشرفته مبتنی بر وب با کد باز که برای مدیریت سرور Xray-core طراحی شده است. این پنل یک رابط کاربری آسان برای پیکربندی و نظارت بر پروتکل‌های مختلف VPN و پراکسی ارائه می‌دهد. **3X-UI** — یک پنل کنترل پیشرفته مبتنی بر وب با کد باز که برای مدیریت سرور Xray-core طراحی شده است. این پنل یک رابط کاربری آسان برای پیکربندی و نظارت بر پروتکل‌های مختلف VPN و پراکسی ارائه می‌دهد.
@ -43,14 +41,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
**اگر این پروژه برای شما مفید است، می‌توانید به آن یک**:star2: بدهید **اگر این پروژه برای شما مفید است، می‌توانید به آن یک**:star2: بدهید
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank"> <p align="left">
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" > <a href="https://buymeacoffee.com/mhsanaei" target="_blank">
</a> <img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
</br> - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener"> - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments"> - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
</a>
## ستاره‌ها در طول زمان ## ستاره‌ها در طول زمان

View file

@ -7,13 +7,11 @@
</picture> </picture>
</p> </p>
[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) [![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions) [![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#) [![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest) [![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
[![Go Report Card](https://goreportcard.com/badge/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. **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.
@ -43,14 +41,15 @@ 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: **If this project is helpful to you, you may wish to give it a**:star2:
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank"> <p align="left">
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" > <a href="https://buymeacoffee.com/mhsanaei" target="_blank">
</a> <img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
</br> - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener"> - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments"> - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
</a>
## Stargazers over Time ## Stargazers over Time

View file

@ -7,13 +7,11 @@
</picture> </picture>
</p> </p>
[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) [![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions) [![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#) [![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest) [![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
**3X-UI** — продвинутая панель управления с открытым исходным кодом на основе веб-интерфейса, разработанная для управления сервером Xray-core. Предоставляет удобный интерфейс для настройки и мониторинга различных VPN и прокси-протоколов. **3X-UI** — продвинутая панель управления с открытым исходным кодом на основе веб-интерфейса, разработанная для управления сервером Xray-core. Предоставляет удобный интерфейс для настройки и мониторинга различных VPN и прокси-протоколов.
@ -43,14 +41,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
**Если этот проект полезен для вас, вы можете поставить ему**:star2: **Если этот проект полезен для вас, вы можете поставить ему**:star2:
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank"> <p align="left">
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" > <a href="https://buymeacoffee.com/mhsanaei" target="_blank">
</a> <img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
</br> - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener"> - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments"> - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
</a>
## Звезды с течением времени ## Звезды с течением времени

View file

@ -7,13 +7,11 @@
</picture> </picture>
</p> </p>
[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) [![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions) [![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#) [![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest) [![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
**3X-UI** — 一个基于网页的高级开源控制面板,专为管理 Xray-core 服务器而设计。它提供了用户友好的界面,用于配置和监控各种 VPN 和代理协议。 **3X-UI** — 一个基于网页的高级开源控制面板,专为管理 Xray-core 服务器而设计。它提供了用户友好的界面,用于配置和监控各种 VPN 和代理协议。
@ -43,14 +41,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
**如果这个项目对您有帮助,您可以给它一个**:star2: **如果这个项目对您有帮助,您可以给它一个**:star2:
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank"> <p align="left">
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" > <a href="https://buymeacoffee.com/mhsanaei" target="_blank">
</a> <img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
</br> - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener"> - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments"> - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
</a>
## 随时间变化的星标数 ## 随时间变化的星标数

View file

@ -1,5 +1,3 @@
// Package config provides configuration management utilities for the 3x-ui panel,
// including version information, logging levels, database paths, and environment variable handling.
package config package config
import ( import (
@ -18,29 +16,24 @@ var version string
//go:embed name //go:embed name
var name string var name string
// LogLevel represents the logging level for the application.
type LogLevel string type LogLevel string
// Logging level constants
const ( const (
Debug LogLevel = "debug" Debug LogLevel = "debug"
Info LogLevel = "info" Info LogLevel = "info"
Notice LogLevel = "notice" Notice LogLevel = "notice"
Warning LogLevel = "warning" Warn LogLevel = "warn"
Error LogLevel = "error" Error LogLevel = "error"
) )
// GetVersion returns the version string of the 3x-ui application.
func GetVersion() string { func GetVersion() string {
return strings.TrimSpace(version) return strings.TrimSpace(version)
} }
// GetName returns the name of the 3x-ui application.
func GetName() string { func GetName() string {
return strings.TrimSpace(name) return strings.TrimSpace(name)
} }
// GetLogLevel returns the current logging level based on environment variables or defaults to Info.
func GetLogLevel() LogLevel { func GetLogLevel() LogLevel {
if IsDebug() { if IsDebug() {
return Debug return Debug
@ -52,12 +45,10 @@ func GetLogLevel() LogLevel {
return LogLevel(logLevel) return LogLevel(logLevel)
} }
// IsDebug returns true if debug mode is enabled via the XUI_DEBUG environment variable.
func IsDebug() bool { func IsDebug() bool {
return os.Getenv("XUI_DEBUG") == "true" 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 { func GetBinFolderPath() string {
binFolderPath := os.Getenv("XUI_BIN_FOLDER") binFolderPath := os.Getenv("XUI_BIN_FOLDER")
if binFolderPath == "" { if binFolderPath == "" {
@ -83,7 +74,6 @@ func getBaseDir() string {
return exeDir return exeDir
} }
// GetDBFolderPath returns the path to the database folder based on environment variables or platform defaults.
func GetDBFolderPath() string { func GetDBFolderPath() string {
dbFolderPath := os.Getenv("XUI_DB_FOLDER") dbFolderPath := os.Getenv("XUI_DB_FOLDER")
if dbFolderPath != "" { if dbFolderPath != "" {
@ -95,19 +85,17 @@ func GetDBFolderPath() string {
return "/etc/x-ui" return "/etc/x-ui"
} }
// GetDBPath returns the full path to the database file.
func GetDBPath() string { func GetDBPath() string {
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName()) 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 { func GetLogFolder() string {
logFolderPath := os.Getenv("XUI_LOG_FOLDER") logFolderPath := os.Getenv("XUI_LOG_FOLDER")
if logFolderPath != "" { if logFolderPath != "" {
return logFolderPath return logFolderPath
} }
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return filepath.Join(".", "log") return getBaseDir()
} }
return "/var/log" return "/var/log"
} }

View file

@ -1 +1 @@
2.8.4 2.6.7

View file

@ -1,5 +1,3 @@
// Package database provides database initialization, migration, and management utilities
// for the 3x-ui panel using GORM with SQLite.
package database package database
import ( import (
@ -11,10 +9,10 @@ import (
"path" "path"
"slices" "slices"
"github.com/mhsanaei/3x-ui/v2/config" "x-ui/config"
"github.com/mhsanaei/3x-ui/v2/database/model" "x-ui/database/model"
"github.com/mhsanaei/3x-ui/v2/util/crypto" "x-ui/util/crypto"
"github.com/mhsanaei/3x-ui/v2/xray" "x-ui/xray"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
@ -47,7 +45,6 @@ func initModels() error {
return nil return nil
} }
// initUser creates a default admin user if the users table is empty.
func initUser() error { func initUser() error {
empty, err := isTableEmpty("users") empty, err := isTableEmpty("users")
if err != nil { if err != nil {
@ -71,7 +68,6 @@ func initUser() error {
return nil return nil
} }
// runSeeders migrates user passwords to bcrypt and records seeder execution to prevent re-running.
func runSeeders(isUsersEmpty bool) error { func runSeeders(isUsersEmpty bool) error {
empty, err := isTableEmpty("history_of_seeders") empty, err := isTableEmpty("history_of_seeders")
if err != nil { if err != nil {
@ -111,14 +107,12 @@ func runSeeders(isUsersEmpty bool) error {
return nil return nil
} }
// isTableEmpty returns true if the named table contains zero rows.
func isTableEmpty(tableName string) (bool, error) { func isTableEmpty(tableName string) (bool, error) {
var count int64 var count int64
err := db.Table(tableName).Count(&count).Error err := db.Table(tableName).Count(&count).Error
return count == 0, err return count == 0, err
} }
// InitDB sets up the database connection, migrates models, and runs seeders.
func InitDB(dbPath string) error { func InitDB(dbPath string) error {
dir := path.Dir(dbPath) dir := path.Dir(dbPath)
err := os.MkdirAll(dir, fs.ModePerm) err := os.MkdirAll(dir, fs.ModePerm)
@ -147,9 +141,6 @@ func InitDB(dbPath string) error {
} }
isUsersEmpty, err := isTableEmpty("users") isUsersEmpty, err := isTableEmpty("users")
if err != nil {
return err
}
if err := initUser(); err != nil { if err := initUser(); err != nil {
return err return err
@ -157,7 +148,6 @@ func InitDB(dbPath string) error {
return runSeeders(isUsersEmpty) return runSeeders(isUsersEmpty)
} }
// CloseDB closes the database connection if it exists.
func CloseDB() error { func CloseDB() error {
if db != nil { if db != nil {
sqlDB, err := db.DB() sqlDB, err := db.DB()
@ -169,17 +159,14 @@ func CloseDB() error {
return nil return nil
} }
// GetDB returns the global GORM database instance.
func GetDB() *gorm.DB { func GetDB() *gorm.DB {
return db return db
} }
// IsNotFound checks if the given error is a GORM record not found error.
func IsNotFound(err error) bool { func IsNotFound(err error) bool {
return err == gorm.ErrRecordNotFound 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) { func IsSQLiteDB(file io.ReaderAt) (bool, error) {
signature := []byte("SQLite format 3\x00") signature := []byte("SQLite format 3\x00")
buf := make([]byte, len(signature)) buf := make([]byte, len(signature))
@ -190,7 +177,6 @@ func IsSQLiteDB(file io.ReaderAt) (bool, error) {
return bytes.Equal(buf, signature), nil return bytes.Equal(buf, signature), nil
} }
// Checkpoint performs a WAL checkpoint on the SQLite database to ensure data consistency.
func Checkpoint() error { func Checkpoint() error {
// Update WAL // Update WAL
err := db.Exec("PRAGMA wal_checkpoint;").Error err := db.Exec("PRAGMA wal_checkpoint;").Error

View file

@ -1,51 +1,44 @@
// Package model defines the database models and data structures used by the 3x-ui panel.
package model package model
import ( import (
"fmt" "fmt"
"github.com/mhsanaei/3x-ui/v2/util/json_util" "x-ui/util/json_util"
"github.com/mhsanaei/3x-ui/v2/xray" "x-ui/xray"
) )
// Protocol represents the protocol type for Xray inbounds.
type Protocol string type Protocol string
// Protocol constants for different Xray inbound protocols
const ( const (
VMESS Protocol = "vmess" VMESS Protocol = "vmess"
VLESS Protocol = "vless" VLESS Protocol = "vless"
Tunnel Protocol = "tunnel" DOKODEMO Protocol = "dokodemo-door"
HTTP Protocol = "http" HTTP Protocol = "http"
Trojan Protocol = "trojan" Trojan Protocol = "trojan"
Shadowsocks Protocol = "shadowsocks" Shadowsocks Protocol = "shadowsocks"
Mixed Protocol = "mixed" Socks Protocol = "socks"
WireGuard Protocol = "wireguard" WireGuard Protocol = "wireguard"
) )
// User represents a user account in the 3x-ui panel.
type User struct { type User struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
} }
// Inbound represents an Xray inbound configuration with traffic statistics and settings.
type Inbound struct { type Inbound struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
UserId int `json:"-"` // Associated user ID UserId int `json:"-"`
Up int64 `json:"up" form:"up"` // Upload traffic in bytes Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"` // Download traffic in bytes Down int64 `json:"down" form:"down"`
Total int64 `json:"total" form:"total"` // Total traffic limit in bytes Total int64 `json:"total" form:"total"`
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` // All-time traffic usage AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"`
Remark string `json:"remark" form:"remark"` // Human-readable remark Remark string `json:"remark" form:"remark"`
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled Enable bool `json:"enable" form:"enable"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2"` // Traffic reset schedule ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`
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
// Xray configuration fields // config part
Listen string `json:"listen" form:"listen"` Listen string `json:"listen" form:"listen"`
Port int `json:"port" form:"port"` Port int `json:"port" form:"port"`
Protocol Protocol `json:"protocol" form:"protocol"` Protocol Protocol `json:"protocol" form:"protocol"`
@ -55,7 +48,6 @@ type Inbound struct {
Sniffing string `json:"sniffing" form:"sniffing"` Sniffing string `json:"sniffing" form:"sniffing"`
} }
// OutboundTraffics tracks traffic statistics for Xray outbound connections.
type OutboundTraffics struct { type OutboundTraffics struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Tag string `json:"tag" form:"tag" gorm:"unique"` Tag string `json:"tag" form:"tag" gorm:"unique"`
@ -64,20 +56,17 @@ type OutboundTraffics struct {
Total int64 `json:"total" form:"total" gorm:"default:0"` Total int64 `json:"total" form:"total" gorm:"default:0"`
} }
// InboundClientIps stores IP addresses associated with inbound clients for access control.
type InboundClientIps struct { type InboundClientIps struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" gorm:"primaryKey;autoIncrement"`
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"` ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
Ips string `json:"ips" form:"ips"` Ips string `json:"ips" form:"ips"`
} }
// HistoryOfSeeders tracks which database seeders have been executed to prevent re-running.
type HistoryOfSeeders struct { type HistoryOfSeeders struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" gorm:"primaryKey;autoIncrement"`
SeederName string `json:"seederName"` SeederName string `json:"seederName"`
} }
// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
listen := i.Listen listen := i.Listen
if listen != "" { if listen != "" {
@ -94,28 +83,26 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
} }
} }
// Setting stores key-value configuration settings for the 3x-ui panel.
type Setting struct { type Setting struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Key string `json:"key" form:"key"` Key string `json:"key" form:"key"`
Value string `json:"value" form:"value"` Value string `json:"value" form:"value"`
} }
// Client represents a client configuration for Xray inbounds with traffic limits and settings.
type Client struct { type Client struct {
ID string `json:"id"` // Unique client identifier ID string `json:"id"`
Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm") Security string `json:"security"`
Password string `json:"password"` // Client password Password string `json:"password"`
Flow string `json:"flow"` // Flow control (XTLS) Flow string `json:"flow"`
Email string `json:"email"` // Client email identifier Email string `json:"email"`
LimitIP int `json:"limitIp"` // IP limit for this client LimitIP int `json:"limitIp"`
TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB TotalGB int64 `json:"totalGB" form:"totalGB"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
Enable bool `json:"enable" form:"enable"` // Whether the client is enabled Enable bool `json:"enable" form:"enable"`
TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications TgID int64 `json:"tgId" form:"tgId"`
SubID string `json:"subId" form:"subId"` // Subscription identifier SubID string `json:"subId" form:"subId"`
Comment string `json:"comment" form:"comment"` // Client comment Comment string `json:"comment" form:"comment"`
Reset int `json:"reset" form:"reset"` // Reset period in days Reset int `json:"reset" form:"reset"`
CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp CreatedAt int64 `json:"created_at,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp UpdatedAt int64 `json:"updated_at,omitempty"`
} }

60
go.mod
View file

@ -1,12 +1,11 @@
module github.com/mhsanaei/3x-ui/v2 module x-ui
go 1.25.1 go 1.25.0
require ( require (
github.com/gin-contrib/gzip v1.2.3 github.com/gin-contrib/gzip v1.2.3
github.com/gin-contrib/sessions v1.0.4 github.com/gin-contrib/sessions v1.0.4
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.10.1
github.com/go-ldap/ldap/v3 v3.4.12
github.com/goccy/go-json v0.10.5 github.com/goccy/go-json v0.10.5
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
@ -15,38 +14,32 @@ require (
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.9 github.com/shirou/gopsutil/v4 v4.25.7
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/valyala/fasthttp v1.65.0
github.com/valyala/fasthttp v1.66.0
github.com/xlzd/gotp v0.1.0 github.com/xlzd/gotp v0.1.0
github.com/xtls/xray-core v1.250911.0 github.com/xtls/xray-core v1.250803.1-0.20250829143322-81b7cd718ad5
go.uber.org/atomic v1.11.0 go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.42.0 golang.org/x/crypto v0.41.0
golang.org/x/sys v0.36.0 golang.org/x/text v0.28.0
golang.org/x/text v0.29.0 google.golang.org/grpc v1.75.0
google.golang.org/grpc v1.76.0
gorm.io/driver/sqlite v1.6.0 gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.0 gorm.io/gorm v1.30.2
) )
require ( require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
github.com/ebitengine/purego v0.9.0 // indirect github.com/ebitengine/purego v0.8.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // 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-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
@ -70,12 +63,12 @@ require (
github.com/pires/go-proxyproto v0.8.1 // indirect github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect github.com/quic-go/quic-go v0.54.0 // indirect
github.com/refraction-networking/utls v1.8.0 // indirect github.com/refraction-networking/utls v1.8.0 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagernet/sing v0.7.12 // indirect github.com/sagernet/sing v0.7.5 // indirect
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect github.com/sagernet/sing-shadowsocks v0.2.8 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect github.com/tklauser/numcpus v0.10.0 // indirect
@ -86,19 +79,22 @@ require (
github.com/valyala/fastjson v1.6.4 // indirect github.com/valyala/fastjson v1.6.4 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196 // indirect github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/mock v0.6.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.21.0 // indirect golang.org/x/arch v0.20.0 // indirect
golang.org/x/mod v0.28.0 // indirect golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.44.0 // indirect golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/time v0.13.0 // indirect golang.org/x/sys v0.35.0 // indirect
golang.org/x/tools v0.37.0 // indirect golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
google.golang.org/protobuf v1.36.10 // indirect google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
lukechampine.com/blake3 v1.4.1 // indirect lukechampine.com/blake3 v1.4.1 // indirect
) )

124
go.sum
View file

@ -1,15 +1,9 @@
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/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 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
@ -23,8 +17,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= 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 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.10/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 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
@ -35,12 +29,8 @@ github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kb
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= 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 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
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 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@ -54,12 +44,10 @@ 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/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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/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 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@ -83,20 +71,6 @@ 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/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 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek= 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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@ -148,8 +122,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE= github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
@ -158,16 +132,14 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagernet/sing v0.7.12 h1:MpMbO56crPRZTbltoj1wGk4Xj9+GiwH1wTO4s3fz1EA= github.com/sagernet/sing v0.7.5 h1:gNMwZCLPqR+4e0g6dwi0sSsrvOmoMjpZgqxKsuJZatc=
github.com/sagernet/sing v0.7.12/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.7.5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4= github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU= github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8= github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -190,8 +162,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNuyU= github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs= github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
@ -200,10 +172,10 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po= github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196 h1:jb1y+Rm6UBW/CEV0FehsKlQ/2dnLsQjyUjn3UfWwbic= github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f h1:o1Kryl9qEYYzNep9RId9DM1kBn8tBrcK5UJnti/l0NI=
github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0= github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno= github.com/xtls/xray-core v1.250803.1-0.20250829143322-81b7cd718ad5 h1:rBqCVgic8yIUVHB4h26K8JNuwJuNj45egsdXxwEvA7E=
github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k= github.com/xtls/xray-core v1.250803.1-0.20250829143322-81b7cd718ad5/go.mod h1:WB/73DmN9Vs7lxtx4Xc/D0Ub1VUu06hAh1mMh8JN2uM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@ -226,42 +198,42 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= 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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87 h1:WgGZrMngVRRve7T3P5gbXdmedSmUpkf8uIUu1fg+biY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -273,8 +245,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= gorm.io/gorm v1.30.2 h1:f7bevlVoVe4Byu3pmbWPVHnPsLoWaMjEb7/clyr9Ivs=
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.30.2/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI= gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=

View file

@ -53,12 +53,9 @@ install_base() {
arch | manjaro | parch) arch | manjaro | parch)
pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata
;; ;;
opensuse-tumbleweed | opensuse-leap) opensuse-tumbleweed)
zypper refresh && zypper -q install -y wget curl tar timezone zypper refresh && zypper -q install -y wget curl tar timezone
;; ;;
alpine)
apk update && apk add wget curl tar tzdata
;;
*) *)
apt-get update && apt-get install -y -q wget curl tar tzdata apt-get update && apt-get install -y -q wget curl tar tzdata
;; ;;
@ -149,15 +146,11 @@ install_x-ui() {
if [ $# == 0 ]; then if [ $# == 0 ]; then
tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$tag_version" ]]; then if [[ ! -n "$tag_version" ]]; then
echo -e "${yellow}Trying to fetch version with IPv4...${plain}" echo -e "${red}Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later${plain}"
tag_version=$(curl -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') exit 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 fi
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..." echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
wget --inet4-only -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 wget -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}" echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}"
exit 1 exit 1
@ -174,25 +167,17 @@ install_x-ui() {
url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz" 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" echo -e "Beginning to install x-ui $1"
wget --inet4-only -N -O /usr/local/x-ui-linux-$(arch).tar.gz ${url} wget -N -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}" echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}"
exit 1 exit 1
fi fi
fi fi
wget --inet4-only -O /usr/bin/x-ui-temp https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh wget -O /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 # Stop x-ui service and remove old resources
if [[ -e /usr/local/x-ui/ ]]; then if [[ -e /usr/local/x-ui/ ]]; then
if [[ $release == "alpine" ]]; then systemctl stop x-ui
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm /usr/local/x-ui/ -rf rm /usr/local/x-ui/ -rf
fi fi
@ -216,22 +201,10 @@ install_x-ui() {
chmod +x /usr/bin/x-ui chmod +x /usr/bin/x-ui
config_after_install config_after_install
if [[ $release == "alpine" ]]; then cp -f x-ui.service /etc/systemd/system/
wget --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc systemctl daemon-reload
if [[ $? -ne 0 ]]; then systemctl enable x-ui
echo -e "${red}Failed to download x-ui.rc${plain}" systemctl start x-ui
exit 1
fi
chmod +x /etc/init.d/x-ui
rc-update add x-ui
rc-service x-ui start
else
cp -f x-ui.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable x-ui
systemctl start x-ui
fi
echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..." echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
echo -e "" echo -e ""
echo -e "┌───────────────────────────────────────────────────────┐ echo -e "┌───────────────────────────────────────────────────────┐

View file

@ -1,29 +1,15 @@
// 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 package logger
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"runtime"
"time" "time"
"github.com/mhsanaei/3x-ui/v2/config"
"github.com/op/go-logging" "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 ( var (
logger *logging.Logger logger *logging.Logger
logFile *os.File
// logBuffer maintains recent log entries in memory for web UI retrieval
logBuffer []struct { logBuffer []struct {
time string time string
level logging.Level level logging.Level
@ -31,164 +17,89 @@ var (
} }
) )
// InitLogger initializes dual logging backends: console/syslog and file. func init() {
// Console logging uses the specified level, file logging always uses DEBUG level. InitLogger(logging.INFO)
}
func InitLogger(level logging.Level) { func InitLogger(level logging.Level) {
newLogger := logging.MustGetLogger("x-ui") newLogger := logging.MustGetLogger("x-ui")
backends := make([]logging.Backend, 0, 2) var err error
var backend logging.Backend
var format logging.Formatter
ppid := os.Getppid()
// Console/syslog backend with configurable level backend, err = logging.NewSyslogBackend("")
if consoleBackend := initDefaultBackend(); consoleBackend != nil { if err != nil {
leveledBackend := logging.AddModuleLevel(consoleBackend) println(err)
leveledBackend.SetLevel(level, "x-ui") backend = logging.NewLogBackend(os.Stderr, "", 0)
backends = append(backends, leveledBackend) }
if ppid > 0 && err != nil {
format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`)
} else {
format = logging.MustStringFormatter(`%{level} - %{message}`)
} }
// File backend with DEBUG level for comprehensive logging backendFormatter := logging.NewBackendFormatter(backend, format)
if fileBackend := initFileBackend(); fileBackend != nil { backendLeveled := logging.AddModuleLevel(backendFormatter)
leveledBackend := logging.AddModuleLevel(fileBackend) backendLeveled.SetLevel(level, "x-ui")
leveledBackend.SetLevel(logging.DEBUG, "x-ui") newLogger.SetBackend(backendLeveled)
backends = append(backends, leveledBackend)
}
multiBackend := logging.MultiLogger(backends...)
newLogger.SetBackend(multiBackend)
logger = newLogger 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) { func Debug(args ...any) {
logger.Debug(args...) logger.Debug(args...)
addToBuffer("DEBUG", fmt.Sprint(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) { func Debugf(format string, args ...any) {
logger.Debugf(format, args...) logger.Debugf(format, args...)
addToBuffer("DEBUG", fmt.Sprintf(format, args...)) addToBuffer("DEBUG", fmt.Sprintf(format, args...))
} }
// Info logs an info message and adds it to the log buffer.
func Info(args ...any) { func Info(args ...any) {
logger.Info(args...) logger.Info(args...)
addToBuffer("INFO", fmt.Sprint(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) { func Infof(format string, args ...any) {
logger.Infof(format, args...) logger.Infof(format, args...)
addToBuffer("INFO", fmt.Sprintf(format, args...)) addToBuffer("INFO", fmt.Sprintf(format, args...))
} }
// Notice logs a notice message and adds it to the log buffer.
func Notice(args ...any) { func Notice(args ...any) {
logger.Notice(args...) logger.Notice(args...)
addToBuffer("NOTICE", fmt.Sprint(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) { func Noticef(format string, args ...any) {
logger.Noticef(format, args...) logger.Noticef(format, args...)
addToBuffer("NOTICE", fmt.Sprintf(format, args...)) addToBuffer("NOTICE", fmt.Sprintf(format, args...))
} }
// Warning logs a warning message and adds it to the log buffer.
func Warning(args ...any) { func Warning(args ...any) {
logger.Warning(args...) logger.Warning(args...)
addToBuffer("WARNING", fmt.Sprint(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) { func Warningf(format string, args ...any) {
logger.Warningf(format, args...) logger.Warningf(format, args...)
addToBuffer("WARNING", fmt.Sprintf(format, args...)) addToBuffer("WARNING", fmt.Sprintf(format, args...))
} }
// Error logs an error message and adds it to the log buffer.
func Error(args ...any) { func Error(args ...any) {
logger.Error(args...) logger.Error(args...)
addToBuffer("ERROR", fmt.Sprint(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) { func Errorf(format string, args ...any) {
logger.Errorf(format, args...) logger.Errorf(format, args...)
addToBuffer("ERROR", fmt.Sprintf(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) { func addToBuffer(level string, newLog string) {
t := time.Now() t := time.Now()
if len(logBuffer) >= maxLogBufferSize { if len(logBuffer) >= 10240 {
logBuffer = logBuffer[1:] logBuffer = logBuffer[1:]
} }
@ -198,13 +109,12 @@ func addToBuffer(level string, newLog string) {
level logging.Level level logging.Level
log string log string
}{ }{
time: t.Format(timeFormat), time: t.Format("2006/01/02 15:04:05"),
level: logLevel, level: logLevel,
log: newLog, 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 { func GetLogs(c int, level string) []string {
var output []string var output []string
logLevel, _ := logging.LogLevel(level) logLevel, _ := logging.LogLevel(level)

32
main.go
View file

@ -1,5 +1,3 @@
// 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 package main
import ( import (
@ -11,20 +9,19 @@ import (
"syscall" "syscall"
_ "unsafe" _ "unsafe"
"github.com/mhsanaei/3x-ui/v2/config" "x-ui/config"
"github.com/mhsanaei/3x-ui/v2/database" "x-ui/database"
"github.com/mhsanaei/3x-ui/v2/logger" "x-ui/logger"
"github.com/mhsanaei/3x-ui/v2/sub" "x-ui/sub"
"github.com/mhsanaei/3x-ui/v2/util/crypto" "x-ui/util/crypto"
"github.com/mhsanaei/3x-ui/v2/web" "x-ui/web"
"github.com/mhsanaei/3x-ui/v2/web/global" "x-ui/web/global"
"github.com/mhsanaei/3x-ui/v2/web/service" "x-ui/web/service"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/op/go-logging" "github.com/op/go-logging"
) )
// runWebServer initializes and starts the web server for the 3x-ui panel.
func runWebServer() { func runWebServer() {
log.Printf("Starting %v %v", config.GetName(), config.GetVersion()) log.Printf("Starting %v %v", config.GetName(), config.GetVersion())
@ -35,7 +32,7 @@ func runWebServer() {
logger.InitLogger(logging.INFO) logger.InitLogger(logging.INFO)
case config.Notice: case config.Notice:
logger.InitLogger(logging.NOTICE) logger.InitLogger(logging.NOTICE)
case config.Warning: case config.Warn:
logger.InitLogger(logging.WARNING) logger.InitLogger(logging.WARNING)
case config.Error: case config.Error:
logger.InitLogger(logging.ERROR) logger.InitLogger(logging.ERROR)
@ -114,7 +111,6 @@ func runWebServer() {
} }
} }
// resetSetting resets all panel settings to their default values.
func resetSetting() { func resetSetting() {
err := database.InitDB(config.GetDBPath()) err := database.InitDB(config.GetDBPath())
if err != nil { if err != nil {
@ -131,7 +127,6 @@ func resetSetting() {
} }
} }
// showSetting displays the current panel settings if show is true.
func showSetting(show bool) { func showSetting(show bool) {
if show { if show {
settingService := service.SettingService{} settingService := service.SettingService{}
@ -181,7 +176,6 @@ func showSetting(show bool) {
} }
} }
// updateTgbotEnableSts enables or disables the Telegram bot notifications based on the status parameter.
func updateTgbotEnableSts(status bool) { func updateTgbotEnableSts(status bool) {
settingService := service.SettingService{} settingService := service.SettingService{}
currentTgSts, err := settingService.GetTgbotEnabled() currentTgSts, err := settingService.GetTgbotEnabled()
@ -201,7 +195,6 @@ func updateTgbotEnableSts(status bool) {
} }
} }
// updateTgbotSetting updates Telegram bot settings including token, chat ID, and runtime schedule.
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) { func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
err := database.InitDB(config.GetDBPath()) err := database.InitDB(config.GetDBPath())
if err != nil { if err != nil {
@ -239,7 +232,6 @@ 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) { func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) {
err := database.InitDB(config.GetDBPath()) err := database.InitDB(config.GetDBPath())
if err != nil { if err != nil {
@ -298,7 +290,6 @@ 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) { func updateCert(publicKey string, privateKey string) {
err := database.InitDB(config.GetDBPath()) err := database.InitDB(config.GetDBPath())
if err != nil { if err != nil {
@ -326,7 +317,6 @@ func updateCert(publicKey string, privateKey string) {
} }
} }
// GetCertificate displays the current SSL certificate settings if getCert is true.
func GetCertificate(getCert bool) { func GetCertificate(getCert bool) {
if getCert { if getCert {
settingService := service.SettingService{} settingService := service.SettingService{}
@ -344,7 +334,6 @@ func GetCertificate(getCert bool) {
} }
} }
// GetListenIP displays the current panel listen IP address if getListen is true.
func GetListenIP(getListen bool) { func GetListenIP(getListen bool) {
if getListen { if getListen {
@ -359,7 +348,6 @@ func GetListenIP(getListen bool) {
} }
} }
// migrateDb performs database migration operations for the 3x-ui panel.
func migrateDb() { func migrateDb() {
inboundService := service.InboundService{} inboundService := service.InboundService{}
@ -372,8 +360,6 @@ func migrateDb() {
fmt.Println("Migration done!") 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() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {
runWebServer() runWebServer()

BIN
media/buymeacoffe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

View file

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

View file

@ -1,47 +1,23 @@
// Package sub provides subscription server functionality for the 3x-ui panel,
// including HTTP/HTTPS servers for serving subscription links and JSON configurations.
package sub package sub
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"html/template"
"io" "io"
"io/fs"
"net" "net"
"net/http" "net/http"
"os"
"path/filepath"
"strconv" "strconv"
"strings"
"github.com/mhsanaei/3x-ui/v2/logger" "x-ui/config"
"github.com/mhsanaei/3x-ui/v2/util/common" "x-ui/logger"
webpkg "github.com/mhsanaei/3x-ui/v2/web" "x-ui/util/common"
"github.com/mhsanaei/3x-ui/v2/web/locale" "x-ui/web/middleware"
"github.com/mhsanaei/3x-ui/v2/web/middleware" "x-ui/web/network"
"github.com/mhsanaei/3x-ui/v2/web/network" "x-ui/web/service"
"github.com/mhsanaei/3x-ui/v2/web/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// setEmbeddedTemplates parses and sets embedded templates on the engine
func setEmbeddedTemplates(engine *gin.Engine) error {
t, err := template.New("").Funcs(engine.FuncMap).ParseFS(
webpkg.EmbeddedHTML(),
"html/common/page.html",
"html/component/aThemeSwitch.html",
"html/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 { type Server struct {
httpServer *http.Server httpServer *http.Server
listener net.Listener listener net.Listener
@ -53,7 +29,6 @@ type Server struct {
cancel context.CancelFunc cancel context.CancelFunc
} }
// NewServer creates a new subscription server instance with a cancellable context.
func NewServer() *Server { func NewServer() *Server {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
return &Server{ return &Server{
@ -62,13 +37,14 @@ 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) { func (s *Server) initRouter() (*gin.Engine, error) {
// Always run in release mode for the subscription server if config.IsDebug() {
gin.DefaultWriter = io.Discard gin.SetMode(gin.DebugMode)
gin.DefaultErrorWriter = io.Discard } else {
gin.SetMode(gin.ReleaseMode) gin.DefaultWriter = io.Discard
gin.DefaultErrorWriter = io.Discard
gin.SetMode(gin.ReleaseMode)
}
engine := gin.Default() engine := gin.Default()
@ -91,23 +67,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
return nil, err 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() Encrypt, err := s.settingService.GetSubEncrypt()
if err != nil { if err != nil {
return nil, err return nil, err
@ -153,114 +112,15 @@ func (s *Server) initRouter() (*gin.Engine, error) {
SubTitle = "" SubTitle = ""
} }
// set per-request localizer from headers/cookies
engine.Use(locale.LocalizerMiddleware())
// register i18n function similar to web server
i18nWebFunc := func(key string, params ...string) string {
return locale.I18n(locale.Web, key, params...)
}
engine.SetFuncMap(map[string]any{"i18n": i18nWebFunc})
// Templates: prefer embedded; fallback to disk if necessary
if err := setEmbeddedTemplates(engine); err != nil {
logger.Warning("sub: failed to parse embedded templates:", err)
if files, derr := s.getHtmlFiles(); derr == nil {
engine.LoadHTMLFiles(files...)
} else {
logger.Error("sub: no templates available (embedded parse and disk load failed)", err, derr)
}
}
// Assets: use disk if present, fallback to embedded
// 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("/") g := engine.Group("/")
s.sub = NewSUBController( s.sub = NewSUBController(
g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates, g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates,
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle) SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle)
return engine, nil return engine, nil
} }
// getHtmlFiles loads templates from local folder (used in debug mode)
func (s *Server) getHtmlFiles() ([]string, error) {
dir, _ := os.Getwd()
files := []string{}
// common layout
common := filepath.Join(dir, "web", "html", "common", "page.html")
if _, err := os.Stat(common); err == nil {
files = append(files, common)
}
// components used
theme := filepath.Join(dir, "web", "html", "component", "aThemeSwitch.html")
if _, err := os.Stat(theme); err == nil {
files = append(files, theme)
}
// page itself
page := filepath.Join(dir, "web", "html", "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) { func (s *Server) Start() (err error) {
// This is an anonymous function, no function name // This is an anonymous function, no function name
defer func() { defer func() {
@ -334,7 +194,6 @@ func (s *Server) Start() (err error) {
return nil return nil
} }
// Stop gracefully shuts down the subscription server and closes the listener.
func (s *Server) Stop() error { func (s *Server) Stop() error {
s.cancel() s.cancel()
@ -349,7 +208,6 @@ func (s *Server) Stop() error {
return common.Combine(err1, err2) return common.Combine(err1, err2)
} }
// GetCtx returns the server's context for cancellation and deadline management.
func (s *Server) GetCtx() context.Context { func (s *Server) GetCtx() context.Context {
return s.ctx return s.ctx
} }

View file

@ -2,20 +2,16 @@ package sub
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "net"
"strings" "strings"
"github.com/mhsanaei/3x-ui/v2/config"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// SUBController handles HTTP requests for subscription links and JSON configurations.
type SUBController struct { type SUBController struct {
subTitle string subTitle string
subPath string subPath string
subJsonPath string subJsonPath string
jsonEnabled bool
subEncrypt bool subEncrypt bool
updateInterval string updateInterval string
@ -23,12 +19,10 @@ type SUBController struct {
subJsonService *SubJsonService subJsonService *SubJsonService
} }
// NewSUBController creates a new subscription controller with the given configuration.
func NewSUBController( func NewSUBController(
g *gin.RouterGroup, g *gin.RouterGroup,
subPath string, subPath string,
jsonPath string, jsonPath string,
jsonEnabled bool,
encrypt bool, encrypt bool,
showInfo bool, showInfo bool,
rModel string, rModel string,
@ -44,7 +38,6 @@ func NewSUBController(
subTitle: subTitle, subTitle: subTitle,
subPath: subPath, subPath: subPath,
subJsonPath: jsonPath, subJsonPath: jsonPath,
jsonEnabled: jsonEnabled,
subEncrypt: encrypt, subEncrypt: encrypt,
updateInterval: update, updateInterval: update,
@ -55,22 +48,32 @@ func NewSUBController(
return a return a
} }
// initRouter registers HTTP routes for subscription links and JSON endpoints
// on the provided router group.
func (a *SUBController) initRouter(g *gin.RouterGroup) { func (a *SUBController) initRouter(g *gin.RouterGroup) {
gLink := g.Group(a.subPath) gLink := g.Group(a.subPath)
gJson := g.Group(a.subJsonPath)
gLink.GET(":subid", a.subs) gLink.GET(":subid", a.subs)
if a.jsonEnabled {
gJson := g.Group(a.subJsonPath) gJson.GET(":subid", a.subJsons)
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) { func (a *SUBController) subs(c *gin.Context) {
subId := c.Param("subid") subId := c.Param("subid")
scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c) var host string
subs, lastOnline, traffic, err := a.subService.GetSubs(subId, host) 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)
if err != nil || len(subs) == 0 { if err != nil || len(subs) == 0 {
c.String(400, "Error!") c.String(400, "Error!")
} else { } else {
@ -79,55 +82,10 @@ func (a *SUBController) subs(c *gin.Context) {
result += sub + "\n" result += sub + "\n"
} }
// If the request expects HTML (e.g., browser) or explicitly asked (?html=1 or ?view=html), render the info page here
accept := c.GetHeader("Accept")
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
// Build page data in service
subURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId)
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 // Add headers
header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) c.Writer.Header().Set("Subscription-Userinfo", header)
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle) c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
c.Writer.Header().Set("Profile-Title", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
if a.subEncrypt { if a.subEncrypt {
c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
@ -137,25 +95,43 @@ func (a *SUBController) subs(c *gin.Context) {
} }
} }
// subJsons handles HTTP requests for JSON subscription configurations.
func (a *SUBController) subJsons(c *gin.Context) { func (a *SUBController) subJsons(c *gin.Context) {
subId := c.Param("subid") subId := c.Param("subid")
_, host, _, _ := a.subService.ResolveRequest(c) 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
}
}
jsonSub, header, err := a.subJsonService.GetJson(subId, host) jsonSub, header, err := a.subJsonService.GetJson(subId, host)
if err != nil || len(jsonSub) == 0 { if err != nil || len(jsonSub) == 0 {
c.String(400, "Error!") c.String(400, "Error!")
} else { } else {
// Add headers // Add headers
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle) 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)))
c.String(200, jsonSub) c.String(200, jsonSub)
} }
} }
// ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title. func getHostFromXFH(s string) (string, error) {
func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) { if strings.Contains(s, ":") {
c.Writer.Header().Set("Subscription-Userinfo", header) realHost, _, err := net.SplitHostPort(s)
c.Writer.Header().Set("Profile-Update-Interval", updateInterval) if err != nil {
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle))) return "", err
}
return realHost, nil
}
return s, nil
} }

View file

@ -6,18 +6,17 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/mhsanaei/3x-ui/v2/database/model" "x-ui/database/model"
"github.com/mhsanaei/3x-ui/v2/logger" "x-ui/logger"
"github.com/mhsanaei/3x-ui/v2/util/json_util" "x-ui/util/json_util"
"github.com/mhsanaei/3x-ui/v2/util/random" "x-ui/util/random"
"github.com/mhsanaei/3x-ui/v2/web/service" "x-ui/web/service"
"github.com/mhsanaei/3x-ui/v2/xray" "x-ui/xray"
) )
//go:embed default.json //go:embed default.json
var defaultJson string var defaultJson string
// SubJsonService handles JSON subscription configuration generation and management.
type SubJsonService struct { type SubJsonService struct {
configJson map[string]any configJson map[string]any
defaultOutbounds []json_util.RawMessage defaultOutbounds []json_util.RawMessage
@ -29,7 +28,6 @@ type SubJsonService struct {
SubService *SubService 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 { func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService {
var configJson map[string]any var configJson map[string]any
var defaultOutbounds []json_util.RawMessage var defaultOutbounds []json_util.RawMessage
@ -69,7 +67,6 @@ 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) { func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
inbounds, err := s.SubService.getInboundsBySubId(subId) inbounds, err := s.SubService.getInboundsBySubId(subId)
if err != nil || len(inbounds) == 0 { if err != nil || len(inbounds) == 0 {
@ -174,12 +171,12 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
case "tls": case "tls":
if newStream["security"] != "tls" { if newStream["security"] != "tls" {
newStream["security"] = "tls" newStream["security"] = "tls"
newStream["tlsSettings"] = map[string]any{} newStream["tslSettings"] = map[string]any{}
} }
case "none": case "none":
if newStream["security"] != "none" { if newStream["security"] != "none" {
newStream["security"] = "none" newStream["security"] = "none"
delete(newStream, "tlsSettings") delete(newStream, "tslSettings")
} }
} }
streamSettings, _ := json.MarshalIndent(newStream, "", " ") streamSettings, _ := json.MarshalIndent(newStream, "", " ")
@ -187,10 +184,8 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
var newOutbounds []json_util.RawMessage var newOutbounds []json_util.RawMessage
switch inbound.Protocol { switch inbound.Protocol {
case "vmess": case "vmess", "vless":
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client)) newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client))
case "vless":
newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client))
case "trojan", "shadowsocks": case "trojan", "shadowsocks":
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client)) newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
} }
@ -294,8 +289,15 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
usersData := make([]UserVnext, 1) usersData := make([]UserVnext, 1)
usersData[0].ID = client.ID usersData[0].ID = client.ID
usersData[0].Email = client.Email usersData[0].Level = 8
usersData[0].Security = client.Security if inbound.Protocol == model.VMESS {
usersData[0].Security = client.Security
}
if inbound.Protocol == model.VLESS {
usersData[0].Flow = client.Flow
usersData[0].Encryption = "none"
}
vnextData := make([]VnextSetting, 1) vnextData := make([]VnextSetting, 1)
vnextData[0] = VnextSetting{ vnextData[0] = VnextSetting{
Address: inbound.Listen, Address: inbound.Listen,
@ -309,42 +311,14 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
outbound.Mux = json_util.RawMessage(s.mux) outbound.Mux = json_util.RawMessage(s.mux)
} }
outbound.StreamSettings = streamSettings outbound.StreamSettings = streamSettings
outbound.Settings = map[string]any{ outbound.Settings = OutboundSettings{
"vnext": vnextData, Vnext: vnextData,
} }
result, _ := json.MarshalIndent(outbound, "", " ") result, _ := json.MarshalIndent(outbound, "", " ")
return result 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 { func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
outbound := Outbound{} outbound := Outbound{}
@ -376,8 +350,8 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
outbound.Mux = json_util.RawMessage(s.mux) outbound.Mux = json_util.RawMessage(s.mux)
} }
outbound.StreamSettings = streamSettings outbound.StreamSettings = streamSettings
outbound.Settings = map[string]any{ outbound.Settings = OutboundSettings{
"servers": serverData, Servers: serverData,
} }
result, _ := json.MarshalIndent(outbound, "", " ") result, _ := json.MarshalIndent(outbound, "", " ")
@ -389,7 +363,13 @@ type Outbound struct {
Tag string `json:"tag"` Tag string `json:"tag"`
StreamSettings json_util.RawMessage `json:"streamSettings"` StreamSettings json_util.RawMessage `json:"streamSettings"`
Mux json_util.RawMessage `json:"mux,omitempty"` Mux json_util.RawMessage `json:"mux,omitempty"`
Settings map[string]any `json:"settings,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"`
} }
type VnextSetting struct { type VnextSetting struct {
@ -399,9 +379,11 @@ type VnextSetting struct {
} }
type UserVnext struct { type UserVnext struct {
ID string `json:"id"` Encryption string `json:"encryption,omitempty"`
Email string `json:"email,omitempty"` Flow string `json:"flow,omitempty"`
Security string `json:"security,omitempty"` ID string `json:"id"`
Security string `json:"security,omitempty"`
Level int `json:"level"`
} }
type ServerSetting struct { type ServerSetting struct {

View file

@ -3,24 +3,21 @@ package sub
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"net"
"net/url" "net/url"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "x-ui/database"
"github.com/goccy/go-json" "x-ui/database/model"
"x-ui/logger"
"x-ui/util/common"
"x-ui/util/random"
"x-ui/web/service"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/v2/database" "github.com/goccy/go-json"
"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 { type SubService struct {
address string address string
showInfo bool showInfo bool
@ -30,7 +27,6 @@ type SubService struct {
settingService service.SettingService settingService service.SettingService
} }
// NewSubService creates a new subscription service with the given configuration.
func NewSubService(showInfo bool, remarkModel string) *SubService { func NewSubService(showInfo bool, remarkModel string) *SubService {
return &SubService{ return &SubService{
showInfo: showInfo, showInfo: showInfo,
@ -38,20 +34,19 @@ func NewSubService(showInfo bool, remarkModel string) *SubService {
} }
} }
// GetSubs retrieves subscription links for a given subscription ID and host. func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.ClientTraffic, error) {
s.address = host s.address = host
var result []string var result []string
var header string
var traffic xray.ClientTraffic var traffic xray.ClientTraffic
var lastOnline int64
var clientTraffics []xray.ClientTraffic var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId) inbounds, err := s.getInboundsBySubId(subId)
if err != nil { if err != nil {
return nil, 0, traffic, err return nil, "", err
} }
if len(inbounds) == 0 { if len(inbounds) == 0 {
return nil, 0, traffic, common.NewError("No inbounds found with ", subId) return nil, "", common.NewError("No inbounds found with ", subId)
} }
s.datepicker, err = s.settingService.GetDatepicker() s.datepicker, err = s.settingService.GetDatepicker()
@ -78,11 +73,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
if client.Enable && client.SubID == subId { if client.Enable && client.SubID == subId {
link := s.getLink(inbound, client.Email) link := s.getLink(inbound, client.Email)
result = append(result, link) result = append(result, link)
ct := s.getClientTraffics(inbound.ClientStats, client.Email) clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
clientTraffics = append(clientTraffics, ct)
if ct.LastOnline > lastOnline {
lastOnline = ct.LastOnline
}
} }
} }
} }
@ -109,7 +100,8 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
} }
} }
} }
return result, lastOnline, traffic, nil header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
return result, header, nil
} }
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) { func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
@ -337,13 +329,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params := make(map[string]string) params := make(map[string]string)
params["type"] = streamNetwork 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 { switch streamNetwork {
case "tcp": case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]any) tcp, _ := stream["tcpSettings"].(map[string]any)
@ -1010,189 +995,3 @@ func searchHost(headers any) string {
return "" 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
}

258
update.sh
View file

@ -1,258 +0,0 @@
#!/bin/bash
red='\033[0;31m'
green='\033[0;32m'
blue='\033[0;34m'
yellow='\033[0;33m'
plain='\033[0m'
# 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 wget; then
wget_bin=$(which wget)
else
_fail "ERROR: Command 'wget' not found."
fi
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)"
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 wget curl tar tzdata >/dev/null 2>&1
;;
centos | rhel | almalinux | rocky | ol)
yum -y update >/dev/null 2>&1 && yum install -y -q wget curl tar tzdata >/dev/null 2>&1
;;
fedora | amzn | virtuozzo)
dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata >/dev/null 2>&1
;;
arch | manjaro | parch)
pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm wget curl tar tzdata >/dev/null 2>&1
;;
opensuse-tumbleweed | opensuse-leap)
zypper refresh >/dev/null 2>&1 && zypper -q install -y wget curl tar timezone >/dev/null 2>&1
;;
alpine)
apk update >/dev/null 2>&1 && apk add wget curl tar tzdata >/dev/null 2>&1
;;
*)
apt-get update >/dev/null 2>&1 && apt install -y -q wget curl tar tzdata >/dev/null 2>&1
;;
esac
}
config_after_update() {
echo -e "${yellow}x-ui settings:${plain}"
/usr/local/x-ui/x-ui setting -show true
/usr/local/x-ui/x-ui migrate
}
update_x-ui() {
cd /usr/local/
if [ -f "/usr/local/x-ui/x-ui" ]; then
current_xui_version=$(/usr/local/x-ui/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..."
${wget_bin} -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 2>/dev/null
if [[ $? -ne 0 ]]; then
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
${wget_bin} --inet4-only -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 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 /usr/local/x-ui/ ]]; 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 "/etc/systemd/system/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 /etc/systemd/system/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 /usr/bin/x-ui -f >/dev/null 2>&1
rm /usr/local/x-ui/x-ui.service -f >/dev/null 2>&1
rm /usr/local/x-ui/x-ui -f >/dev/null 2>&1
rm /usr/local/x-ui/x-ui.sh -f >/dev/null 2>&1
echo -e "${green}Removing old xray version...${plain}"
rm /usr/local/x-ui/bin/xray-linux-amd64 -f >/dev/null 2>&1
echo -e "${green}Removing old README and LICENSE file...${plain}"
rm /usr/local/x-ui/bin/README.md -f >/dev/null 2>&1
rm /usr/local/x-ui/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}"
${wget_bin} -O /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}"
${wget_bin} --inet4-only -O /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 /usr/local/x-ui/x-ui.sh >/dev/null 2>&1
chmod +x /usr/bin/x-ui >/dev/null 2>&1
echo -e "${green}Changing owner...${plain}"
chown -R root:root /usr/local/x-ui >/dev/null 2>&1
if [ -f "/usr/local/x-ui/bin/config.json" ]; then
echo -e "${green}Changing on config file permissions...${plain}"
chmod 640 /usr/local/x-ui/bin/config.json >/dev/null 2>&1
fi
if [[ $release == "alpine" ]]; then
echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}"
${wget_bin} -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
if [[ $? -ne 0 ]]; then
${wget_bin} --inet4-only -O /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
echo -e "${green}Installing systemd unit...${plain}"
cp -f x-ui.service /etc/systemd/system/ >/dev/null 2>&1
chown root:root /etc/systemd/system/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

View file

@ -1,26 +1,22 @@
// Package common provides common utility functions for error handling, formatting, and multi-error management.
package common package common
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/mhsanaei/3x-ui/v2/logger" "x-ui/logger"
) )
// NewErrorf creates a new error with formatted message.
func NewErrorf(format string, a ...any) error { func NewErrorf(format string, a ...any) error {
msg := fmt.Sprintf(format, a...) msg := fmt.Sprintf(format, a...)
return errors.New(msg) return errors.New(msg)
} }
// NewError creates a new error from the given arguments.
func NewError(a ...any) error { func NewError(a ...any) error {
msg := fmt.Sprintln(a...) msg := fmt.Sprintln(a...)
return errors.New(msg) return errors.New(msg)
} }
// Recover handles panic recovery and logs the panic error if a message is provided.
func Recover(msg string) any { func Recover(msg string) any {
panicErr := recover() panicErr := recover()
if panicErr != nil { if panicErr != nil {

View file

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
) )
// FormatTraffic formats traffic bytes into human-readable units (B, KB, MB, GB, TB, PB).
func FormatTraffic(trafficBytes int64) string { func FormatTraffic(trafficBytes int64) string {
units := []string{"B", "KB", "MB", "GB", "TB", "PB"} units := []string{"B", "KB", "MB", "GB", "TB", "PB"}
unitIndex := 0 unitIndex := 0

View file

@ -4,10 +4,8 @@ import (
"strings" "strings"
) )
// multiError represents a collection of errors.
type multiError []error type multiError []error
// Error returns a string representation of all errors joined with " | ".
func (e multiError) Error() string { func (e multiError) Error() string {
var r strings.Builder var r strings.Builder
r.WriteString("multierr: ") r.WriteString("multierr: ")
@ -18,7 +16,6 @@ func (e multiError) Error() string {
return r.String() return r.String()
} }
// Combine combines multiple errors into a single error, filtering out nil errors.
func Combine(maybeError ...error) error { func Combine(maybeError ...error) error {
var errs multiError var errs multiError
for _, err := range maybeError { for _, err := range maybeError {

View file

@ -1,17 +1,14 @@
// Package crypto provides cryptographic utilities for password hashing and verification.
package crypto package crypto
import ( import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
// HashPasswordAsBcrypt generates a bcrypt hash of the given password.
func HashPasswordAsBcrypt(password string) (string, error) { func HashPasswordAsBcrypt(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(hash), err return string(hash), err
} }
// CheckPasswordHash verifies if the given password matches the bcrypt hash.
func CheckPasswordHash(hash, password string) bool { func CheckPasswordHash(hash, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil return err == nil

View file

@ -1,15 +1,12 @@
// Package json_util provides JSON utilities including a custom RawMessage type.
package json_util package json_util
import ( import (
"errors" "errors"
) )
// RawMessage is a custom JSON raw message type that marshals empty slices as "null".
type RawMessage []byte type RawMessage []byte
// MarshalJSON customizes the JSON marshaling behavior for RawMessage. // MarshalJSON: Customize json.RawMessage default behavior
// Empty RawMessage values are marshaled as "null" instead of "[]".
func (m RawMessage) MarshalJSON() ([]byte, error) { func (m RawMessage) MarshalJSON() ([]byte, error) {
if len(m) == 0 { if len(m) == 0 {
return []byte("null"), nil return []byte("null"), nil
@ -17,7 +14,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) {
return m, nil return m, nil
} }
// UnmarshalJSON sets *m to a copy of the JSON data. // UnmarshalJSON: sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error { func (m *RawMessage) UnmarshalJSON(data []byte) error {
if m == nil { if m == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")

View file

@ -1,144 +0,0 @@
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
}

View file

@ -1,9 +1,7 @@
// Package random provides utilities for generating random strings and numbers.
package random package random
import ( import (
"crypto/rand" "math/rand"
"math/big"
) )
var ( var (
@ -15,8 +13,6 @@ var (
allSeq [62]rune 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() { func init() {
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
numSeq[i] = rune('0' + i) numSeq[i] = rune('0' + i)
@ -37,25 +33,14 @@ func init() {
copy(allSeq[len(numSeq)+len(lowerSeq):], upperSeq[:]) 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 { func Seq(n int) string {
runes := make([]rune, n) runes := make([]rune, n)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(allSeq)))) runes[i] = allSeq[rand.Intn(len(allSeq))]
if err != nil {
panic("crypto/rand failed: " + err.Error())
}
runes[i] = allSeq[idx.Int64()]
} }
return string(runes) return string(runes)
} }
// Num generates a random integer between 0 and n-1.
func Num(n int) int { func Num(n int) int {
bn := big.NewInt(int64(n)) return rand.Intn(n)
r, err := rand.Int(rand.Reader, bn)
if err != nil {
panic("crypto/rand failed: " + err.Error())
}
return int(r.Int64())
} }

View file

@ -1,9 +1,7 @@
// Package reflect_util provides reflection utilities for working with struct fields and values.
package reflect_util package reflect_util
import "reflect" import "reflect"
// GetFields returns all struct fields of the given reflect.Type.
func GetFields(t reflect.Type) []reflect.StructField { func GetFields(t reflect.Type) []reflect.StructField {
num := t.NumField() num := t.NumField()
fields := make([]reflect.StructField, 0, num) fields := make([]reflect.StructField, 0, num)
@ -13,7 +11,6 @@ func GetFields(t reflect.Type) []reflect.StructField {
return fields return fields
} }
// GetFieldValues returns all field values of the given reflect.Value.
func GetFieldValues(v reflect.Value) []reflect.Value { func GetFieldValues(v reflect.Value) []reflect.Value {
num := v.NumField() num := v.NumField()
fields := make([]reflect.Value, 0, num) fields := make([]reflect.Value, 0, num)

View file

@ -1,5 +1,3 @@
// Package sys provides system utilities for monitoring network connections and CPU usage.
// Platform-specific implementations are provided for Windows, Linux, and macOS.
package sys package sys
import ( import (

View file

@ -4,12 +4,7 @@
package sys package sys
import ( import (
"encoding/binary"
"fmt"
"sync"
"github.com/shirou/gopsutil/v4/net" "github.com/shirou/gopsutil/v4/net"
"golang.org/x/sys/unix"
) )
func GetTCPCount() (int, error) { func GetTCPCount() (int, error) {
@ -27,69 +22,3 @@ func GetUDPCount() (int, error) {
} }
return len(stats), nil 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
}

View file

@ -4,14 +4,10 @@
package sys package sys
import ( import (
"bufio"
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"os" "os"
"strconv"
"strings"
"sync"
) )
func getLinesNum(filename string) (int, error) { func getLinesNum(filename string) (int, error) {
@ -45,8 +41,6 @@ func getLinesNum(filename string) (int, error) {
return sum, nil 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) { func GetTCPCount() (int, error) {
root := HostProc() root := HostProc()
@ -77,8 +71,6 @@ func GetUDPCount() (int, error) {
return udp4 + udp6, nil 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) { func safeGetLinesNum(path string) (int, error) {
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return 0, nil return 0, nil
@ -87,99 +79,3 @@ func safeGetLinesNum(path string) (int, error) {
} }
return getLinesNum(path) 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
}

View file

@ -5,14 +5,10 @@ package sys
import ( import (
"errors" "errors"
"sync"
"syscall"
"unsafe"
"github.com/shirou/gopsutil/v4/net" "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) { func GetConnectionCount(proto string) (int, error) {
if proto != "tcp" && proto != "udp" { if proto != "tcp" && proto != "udp" {
return 0, errors.New("invalid protocol") return 0, errors.New("invalid protocol")
@ -25,92 +21,10 @@ func GetConnectionCount(proto string) (int, error) {
return len(stats), nil return len(stats), nil
} }
// GetTCPCount returns the number of active TCP connections.
func GetTCPCount() (int, error) { func GetTCPCount() (int, error) {
return GetConnectionCount("tcp") return GetConnectionCount("tcp")
} }
// GetUDPCount returns the number of active UDP connections.
func GetUDPCount() (int, error) { func GetUDPCount() (int, error) {
return GetConnectionCount("udp") 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -10,8 +10,6 @@ class DBInbound {
this.remark = ""; this.remark = "";
this.enable = true; this.enable = true;
this.expiryTime = 0; this.expiryTime = 0;
this.trafficReset = "never";
this.lastTrafficResetTime = 0;
this.listen = ""; this.listen = "";
this.port = 0; this.port = 0;
@ -51,8 +49,8 @@ class DBInbound {
return this.protocol === Protocols.SHADOWSOCKS; return this.protocol === Protocols.SHADOWSOCKS;
} }
get isMixed() { get isSocks() {
return this.protocol === Protocols.MIXED; return this.protocol === Protocols.SOCKS;
} }
get isHTTP() { get isHTTP() {

View file

@ -3,16 +3,18 @@ const Protocols = {
VLESS: 'vless', VLESS: 'vless',
TROJAN: 'trojan', TROJAN: 'trojan',
SHADOWSOCKS: 'shadowsocks', SHADOWSOCKS: 'shadowsocks',
TUNNEL: 'tunnel', DOKODEMO: 'dokodemo-door',
MIXED: 'mixed', SOCKS: 'socks',
HTTP: 'http', HTTP: 'http',
WIREGUARD: 'wireguard', WIREGUARD: 'wireguard',
}; };
const SSMethods = { const SSMethods = {
AES_256_GCM: 'aes-256-gcm', AES_256_GCM: 'aes-256-gcm',
AES_128_GCM: 'aes-128-gcm',
CHACHA20_POLY1305: 'chacha20-poly1305', CHACHA20_POLY1305: 'chacha20-poly1305',
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305', CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
XCHACHA20_POLY1305: 'xchacha20-poly1305',
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305', XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm', BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm', BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
@ -729,7 +731,7 @@ class RealityStreamSettings extends XrayCommonClass {
constructor( constructor(
show = false, show = false,
xver = 0, xver = 0,
target = 'google.com:443', dest = 'google.com:443',
serverNames = 'google.com,www.google.com', serverNames = 'google.com,www.google.com',
privateKey = '', privateKey = '',
minClientVer = '', minClientVer = '',
@ -742,7 +744,7 @@ class RealityStreamSettings extends XrayCommonClass {
super(); super();
this.show = show; this.show = show;
this.xver = xver; this.xver = xver;
this.target = target; this.dest = dest;
this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames; this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames;
this.privateKey = privateKey; this.privateKey = privateKey;
this.minClientVer = minClientVer; this.minClientVer = minClientVer;
@ -767,7 +769,7 @@ class RealityStreamSettings extends XrayCommonClass {
return new RealityStreamSettings( return new RealityStreamSettings(
json.show, json.show,
json.xver, json.xver,
json.target, json.dest,
json.serverNames, json.serverNames,
json.privateKey, json.privateKey,
json.minClientVer, json.minClientVer,
@ -783,7 +785,7 @@ class RealityStreamSettings extends XrayCommonClass {
return { return {
show: this.show, show: this.show,
xver: this.xver, xver: this.xver,
target: this.target, dest: this.dest,
serverNames: this.serverNames.split(","), serverNames: this.serverNames.split(","),
privateKey: this.privateKey, privateKey: this.privateKey,
minClientVer: this.minClientVer, minClientVer: this.minClientVer,
@ -1299,7 +1301,6 @@ class Inbound extends XrayCommonClass {
const security = forceTls == 'same' ? this.stream.security : forceTls; const security = forceTls == 'same' ? this.stream.security : forceTls;
const params = new Map(); const params = new Map();
params.set("type", this.stream.network); params.set("type", this.stream.network);
params.set("encryption", this.settings.encryption);
switch (type) { switch (type) {
case "tcp": case "tcp":
const tcp = this.stream.tcp; const tcp = this.stream.tcp;
@ -1712,8 +1713,8 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.VLESS: return new Inbound.VLESSSettings(protocol); case Protocols.VLESS: return new Inbound.VLESSSettings(protocol);
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol); case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol); case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol); case Protocols.DOKODEMO: return new Inbound.DokodemoSettings(protocol);
case Protocols.MIXED: return new Inbound.MixedSettings(protocol); case Protocols.SOCKS: return new Inbound.SocksSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol); case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol); case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
default: return null; default: return null;
@ -1726,8 +1727,8 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json); case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json);
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json); case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json); case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json); case Protocols.DOKODEMO: return Inbound.DokodemoSettings.fromJson(json);
case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json); case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json); case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json); case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
default: return null; default: return null;
@ -1858,17 +1859,13 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
constructor( constructor(
protocol, protocol,
vlesses = [new Inbound.VLESSSettings.VLESS()], vlesses = [new Inbound.VLESSSettings.VLESS()],
decryption = "none", decryption = 'none',
encryption = "none", fallbacks = []
fallbacks = [],
selectedAuth = undefined,
) { ) {
super(protocol); super(protocol);
this.vlesses = vlesses; this.vlesses = vlesses;
this.decryption = decryption; this.decryption = decryption;
this.encryption = encryption;
this.fallbacks = fallbacks; this.fallbacks = fallbacks;
this.selectedAuth = selectedAuth;
} }
addFallback() { addFallback() {
@ -1879,43 +1876,22 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
this.fallbacks.splice(index, 1); this.fallbacks.splice(index, 1);
} }
// decryption should be set to static value
static fromJson(json = {}) { static fromJson(json = {}) {
const obj = new Inbound.VLESSSettings( return new Inbound.VLESSSettings(
Protocols.VLESS, Protocols.VLESS,
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)), json.clients.map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
json.decryption, json.decryption || 'none',
json.encryption, Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks),);
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
json.selectedAuth
);
return obj;
} }
toJson() { toJson() {
const json = { return {
clients: Inbound.VLESSSettings.toJsonArray(this.vlesses), clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
decryption: this.decryption,
fallbacks: Inbound.VLESSSettings.toJsonArray(this.fallbacks),
}; };
if (this.decryption) {
json.decryption = this.decryption;
}
if (this.encryption) {
json.encryption = this.encryption;
}
if (this.fallbacks && this.fallbacks.length > 0) {
json.fallbacks = Inbound.VLESSSettings.toJsonArray(this.fallbacks);
}
if (this.selectedAuth) {
json.selectedAuth = this.selectedAuth;
}
return json;
} }
}; };
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass { Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
@ -2327,7 +2303,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
}; };
Inbound.TunnelSettings = class extends Inbound.Settings { Inbound.DokodemoSettings = class extends Inbound.Settings {
constructor( constructor(
protocol, protocol,
address, address,
@ -2345,8 +2321,8 @@ Inbound.TunnelSettings = class extends Inbound.Settings {
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.TunnelSettings( return new Inbound.DokodemoSettings(
Protocols.TUNNEL, Protocols.DOKODEMO,
json.address, json.address,
json.port, json.port,
XrayCommonClass.toHeaders(json.portMap), XrayCommonClass.toHeaders(json.portMap),
@ -2366,8 +2342,8 @@ Inbound.TunnelSettings = class extends Inbound.Settings {
} }
}; };
Inbound.MixedSettings = class extends Inbound.Settings { Inbound.SocksSettings = class extends Inbound.Settings {
constructor(protocol, auth = 'password', accounts = [new Inbound.MixedSettings.SocksAccount()], udp = false, ip = '127.0.0.1') { constructor(protocol, auth = 'password', accounts = [new Inbound.SocksSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
super(protocol); super(protocol);
this.auth = auth; this.auth = auth;
this.accounts = accounts; this.accounts = accounts;
@ -2387,11 +2363,11 @@ Inbound.MixedSettings = class extends Inbound.Settings {
let accounts; let accounts;
if (json.auth === 'password') { if (json.auth === 'password') {
accounts = json.accounts.map( accounts = json.accounts.map(
account => Inbound.MixedSettings.SocksAccount.fromJson(account) account => Inbound.SocksSettings.SocksAccount.fromJson(account)
) )
} }
return new Inbound.MixedSettings( return new Inbound.SocksSettings(
Protocols.MIXED, Protocols.SOCKS,
json.auth, json.auth,
accounts, accounts,
json.udp, json.udp,
@ -2408,7 +2384,7 @@ Inbound.MixedSettings = class extends Inbound.Settings {
}; };
} }
}; };
Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass { Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass {
constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) { constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
super(); super();
this.user = user; this.user = user;
@ -2416,7 +2392,7 @@ Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.MixedSettings.SocksAccount(json.user, json.pass); return new Inbound.SocksSettings.SocksAccount(json.user, json.pass);
} }
}; };

View file

@ -647,6 +647,10 @@ class Outbound extends CommonClass {
].includes(this.protocol); ].includes(this.protocol);
} }
hasVnext() {
return [Protocols.VMess, Protocols.VLESS].includes(this.protocol);
}
hasServers() { hasServers() {
return [Protocols.Trojan, Protocols.Shadowsocks, Protocols.Socks, Protocols.HTTP].includes(this.protocol); return [Protocols.Trojan, Protocols.Shadowsocks, Protocols.Socks, Protocols.HTTP].includes(this.protocol);
} }
@ -686,15 +690,13 @@ class Outbound extends CommonClass {
if (this.stream?.sockopt) if (this.stream?.sockopt)
stream = { sockopt: this.stream.sockopt.toJson() }; stream = { sockopt: this.stream.sockopt.toJson() };
} }
let settingsOut = this.settings instanceof CommonClass ? this.settings.toJson() : this.settings;
return { return {
tag: this.tag == '' ? undefined : this.tag,
protocol: this.protocol, protocol: this.protocol,
settings: settingsOut, settings: this.settings instanceof CommonClass ? this.settings.toJson() : this.settings,
// Only include tag, streamSettings, sendThrough, mux if present and not empty streamSettings: stream,
...(this.tag ? { tag: this.tag } : {}), sendThrough: this.sendThrough != "" ? this.sendThrough : undefined,
...(stream ? { streamSettings: stream } : {}), mux: this.mux?.enabled ? this.mux : undefined,
...(this.sendThrough ? { sendThrough: this.sendThrough } : {}),
...(this.mux?.enabled ? { mux: this.mux } : {}),
}; };
} }
@ -811,7 +813,7 @@ class Outbound extends CommonClass {
var settings; var settings;
switch (protocol) { switch (protocol) {
case Protocols.VLESS: case Protocols.VLESS:
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '', url.searchParams.get('encryption') ?? 'none'); settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '');
break; break;
case Protocols.Trojan: case Protocols.Trojan:
settings = new Outbound.TrojanSettings(address, port, userData); settings = new Outbound.TrojanSettings(address, port, userData);
@ -906,7 +908,7 @@ Outbound.FreedomSettings = class extends CommonClass {
toJson() { toJson() {
return { return {
domainStrategy: ObjectUtil.isEmpty(this.domainStrategy) ? undefined : this.domainStrategy, 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, fragment: Object.keys(this.fragment).length === 0 ? undefined : this.fragment,
noises: this.noises.length === 0 ? undefined : Outbound.FreedomSettings.Noise.toJsonArray(this.noises), noises: this.noises.length === 0 ? undefined : Outbound.FreedomSettings.Noise.toJsonArray(this.noises),
}; };
@ -1024,16 +1026,13 @@ Outbound.VmessSettings = class extends CommonClass {
} }
static fromJson(json = {}) { static fromJson(json = {}) {
if (!ObjectUtil.isArrEmpty(json.vnext)) { if (ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VmessSettings();
const v = json.vnext[0] || {}; return new Outbound.VmessSettings(
const u = ObjectUtil.isArrEmpty(v.users) ? {} : v.users[0]; json.vnext[0].address,
return new Outbound.VmessSettings( json.vnext[0].port,
v.address, json.vnext[0].users[0].id,
v.port, json.vnext[0].users[0].security,
u.id, );
u.security,
);
}
} }
toJson() { toJson() {
@ -1041,42 +1040,39 @@ Outbound.VmessSettings = class extends CommonClass {
vnext: [{ vnext: [{
address: this.address, address: this.address,
port: this.port, port: this.port,
users: [{ users: [{ id: this.id, security: this.security }],
id: this.id, }],
security: this.security
}]
}]
}; };
} }
}; };
Outbound.VLESSSettings = class extends CommonClass { Outbound.VLESSSettings = class extends CommonClass {
constructor(address, port, id, flow, encryption) { constructor(address, port, id, flow, encryption = 'none') {
super(); super();
this.address = address; this.address = address;
this.port = port; this.port = port;
this.id = id; this.id = id;
this.flow = flow; this.flow = flow;
this.encryption = encryption; this.encryption = encryption
} }
static fromJson(json = {}) { static fromJson(json = {}) {
if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VLESSSettings(); if (ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VLESSSettings();
return new Outbound.VLESSSettings( return new Outbound.VLESSSettings(
json.address, json.vnext[0].address,
json.port, json.vnext[0].port,
json.id, json.vnext[0].users[0].id,
json.flow, json.vnext[0].users[0].flow,
json.encryption json.vnext[0].users[0].encryption,
); );
} }
toJson() { toJson() {
return { return {
address: this.address, vnext: [{
port: this.port, address: this.address,
id: this.id, port: this.port,
flow: this.flow, users: [{ id: this.id, flow: this.flow, encryption: 'none', }],
encryption: this.encryption, }],
}; };
} }
}; };

View file

@ -8,7 +8,7 @@ class AllSetting {
this.webKeyFile = ""; this.webKeyFile = "";
this.webBasePath = "/"; this.webBasePath = "/";
this.sessionMaxAge = 360; this.sessionMaxAge = 360;
this.pageSize = 25; this.pageSize = 50;
this.expireDiff = 0; this.expireDiff = 0;
this.trafficDiff = 0; this.trafficDiff = 0;
this.remarkModel = "-ieo"; this.remarkModel = "-ieo";
@ -26,8 +26,7 @@ class AllSetting {
this.twoFactorEnable = false; this.twoFactorEnable = false;
this.twoFactorToken = ""; this.twoFactorToken = "";
this.xrayTemplateConfig = ""; this.xrayTemplateConfig = "";
this.subEnable = true; this.subEnable = false;
this.subJsonEnable = false;
this.subTitle = ""; this.subTitle = "";
this.subListen = ""; this.subListen = "";
this.subPort = 2096; this.subPort = 2096;
@ -50,28 +49,6 @@ class AllSetting {
this.timeLocation = "Local"; 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) { if (data == null) {
return return
} }

View file

@ -1,160 +0,0 @@
(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 + '" }}';
},
},
});
})();

View file

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

View file

@ -316,13 +316,15 @@ class ObjectUtil {
} }
static equals(a, b) { static equals(a, b) {
// shallow, symmetric comparison so newly added fields also affect equality for (const key in a) {
const aKeys = Object.keys(a); if (!a.hasOwnProperty(key)) {
const bKeys = Object.keys(b); continue;
if (aKeys.length !== bKeys.length) return false; }
for (const key of aKeys) { if (!b.hasOwnProperty(key)) {
if (!Object.prototype.hasOwnProperty.call(b, key)) return false; return false;
if (a[key] !== b[key]) return false; } else if (a[key] !== b[key]) {
return false;
}
} }
return true; return true;
} }

File diff suppressed because one or more lines are too long

View file

@ -1,19 +1,19 @@
//! otpauth 9.4.1 | (c) Héctor Molinero Fernández | MIT | https://github.com/hectorm/otpauth //! otpauth 9.4.0 | (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 //! noble-hashes 1.7.1 | (c) Paul Miller | MIT | https://github.com/paulmillr/noble-hashes
/// <reference types="./otpauth.d.ts" /> /// <reference types="./otpauth.d.ts" />
// @ts-nocheck // @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){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),{})) !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
;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 ;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%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, ;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]}}
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 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=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 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)
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, },[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.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]}} 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(),
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))} 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){
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}`)} 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):
},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, 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)}),
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 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=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://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=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){ ;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(!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"}); 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 //# sourceMappingURL=otpauth.umd.min.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,58 +1,60 @@
package controller package controller
import ( import (
"net/http" "x-ui/web/service"
"github.com/mhsanaei/3x-ui/v2/web/service"
"github.com/mhsanaei/3x-ui/v2/web/session"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// APIController handles the main API routes for the 3x-ui panel, including inbounds and server management.
type APIController struct { type APIController struct {
BaseController BaseController
inboundController *InboundController inboundController *InboundController
serverController *ServerController
Tgbot service.Tgbot Tgbot service.Tgbot
} }
// NewAPIController creates a new APIController instance and initializes its routes.
func NewAPIController(g *gin.RouterGroup) *APIController { func NewAPIController(g *gin.RouterGroup) *APIController {
a := &APIController{} a := &APIController{}
a.initRouter(g) a.initRouter(g)
return a return a
} }
// 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()
}
// initRouter sets up the API routes for inbounds, server, and other endpoints.
func (a *APIController) initRouter(g *gin.RouterGroup) { func (a *APIController) initRouter(g *gin.RouterGroup) {
// Main API group g = g.Group("/panel/api/inbounds")
api := g.Group("/panel/api") g.Use(a.checkLogin)
api.Use(a.checkAPIAuth)
// Inbounds API a.inboundController = NewInboundController(g)
inbounds := api.Group("/inbounds")
a.inboundController = NewInboundController(inbounds)
// Server API inboundRoutes := []struct {
server := api.Group("/server") Method string
a.serverController = NewServerController(server) 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},
{"POST", "/updateClientTraffic/:email", a.inboundController.updateClientTraffic},
}
// Extra routes for _, route := range inboundRoutes {
api.GET("/backuptotgbot", a.BackuptoTgbot) g.Handle(route.Method, route.Path, route.Handler)
}
} }
// BackuptoTgbot sends a backup of the panel data to Telegram bot admins. func (a *APIController) createBackup(c *gin.Context) {
func (a *APIController) BackuptoTgbot(c *gin.Context) {
a.Tgbot.SendBackupToAdmins() a.Tgbot.SendBackupToAdmins()
} }

View file

@ -1,21 +1,17 @@
// 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 package controller
import ( import (
"net/http" "net/http"
"github.com/mhsanaei/3x-ui/v2/logger" "x-ui/logger"
"github.com/mhsanaei/3x-ui/v2/web/locale" "x-ui/web/locale"
"github.com/mhsanaei/3x-ui/v2/web/session" "x-ui/web/session"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// BaseController provides common functionality for all controllers, including authentication checks.
type BaseController struct{} type BaseController struct{}
// checkLogin is a middleware that verifies user authentication and handles unauthorized access.
func (a *BaseController) checkLogin(c *gin.Context) { func (a *BaseController) checkLogin(c *gin.Context) {
if !session.IsLogin(c) { if !session.IsLogin(c) {
if isAjax(c) { if isAjax(c) {
@ -29,7 +25,6 @@ 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 { func I18nWeb(c *gin.Context, name string, params ...string) string {
anyfunc, funcExists := c.Get("I18n") anyfunc, funcExists := c.Get("I18n")
if !funcExists { if !funcExists {

View file

@ -5,34 +5,28 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"github.com/mhsanaei/3x-ui/v2/database/model" "x-ui/database/model"
"github.com/mhsanaei/3x-ui/v2/web/service" "x-ui/web/service"
"github.com/mhsanaei/3x-ui/v2/web/session" "x-ui/web/session"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// InboundController handles HTTP requests related to Xray inbounds management.
type InboundController struct { type InboundController struct {
inboundService service.InboundService inboundService service.InboundService
xrayService service.XrayService xrayService service.XrayService
} }
// NewInboundController creates a new InboundController and sets up its routes.
func NewInboundController(g *gin.RouterGroup) *InboundController { func NewInboundController(g *gin.RouterGroup) *InboundController {
a := &InboundController{} a := &InboundController{}
a.initRouter(g) a.initRouter(g)
return a return a
} }
// initRouter initializes the routes for inbound-related operations.
func (a *InboundController) initRouter(g *gin.RouterGroup) { func (a *InboundController) initRouter(g *gin.RouterGroup) {
g = g.Group("/inbound")
g.GET("/list", a.getInbounds) g.POST("/list", a.getInbounds)
g.GET("/get/:id", a.getInbound)
g.GET("/getClientTraffics/:email", a.getClientTraffics)
g.GET("/getClientTrafficsById/:id", a.getClientTrafficsById)
g.POST("/add", a.addInbound) g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound) g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound) g.POST("/update/:id", a.updateInbound)
@ -47,12 +41,8 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/delDepletedClients/:id", a.delDepletedClients) g.POST("/delDepletedClients/:id", a.delDepletedClients)
g.POST("/import", a.importInbound) g.POST("/import", a.importInbound)
g.POST("/onlines", a.onlines) g.POST("/onlines", a.onlines)
g.POST("/lastOnline", a.lastOnline)
g.POST("/updateClientTraffic/:email", a.updateClientTraffic)
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
} }
// getInbounds retrieves the list of inbounds for the logged-in user.
func (a *InboundController) getInbounds(c *gin.Context) { func (a *InboundController) getInbounds(c *gin.Context) {
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
inbounds, err := a.inboundService.GetInbounds(user.Id) inbounds, err := a.inboundService.GetInbounds(user.Id)
@ -63,7 +53,6 @@ func (a *InboundController) getInbounds(c *gin.Context) {
jsonObj(c, inbounds, nil) jsonObj(c, inbounds, nil)
} }
// getInbound retrieves a specific inbound by its ID.
func (a *InboundController) getInbound(c *gin.Context) { func (a *InboundController) getInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
@ -78,7 +67,6 @@ func (a *InboundController) getInbound(c *gin.Context) {
jsonObj(c, inbound, nil) jsonObj(c, inbound, nil)
} }
// getClientTraffics retrieves client traffic information by email.
func (a *InboundController) getClientTraffics(c *gin.Context) { func (a *InboundController) getClientTraffics(c *gin.Context) {
email := c.Param("email") email := c.Param("email")
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email) clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
@ -89,7 +77,6 @@ func (a *InboundController) getClientTraffics(c *gin.Context) {
jsonObj(c, clientTraffics, nil) jsonObj(c, clientTraffics, nil)
} }
// getClientTrafficsById retrieves client traffic information by inbound ID.
func (a *InboundController) getClientTrafficsById(c *gin.Context) { func (a *InboundController) getClientTrafficsById(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
clientTraffics, err := a.inboundService.GetClientTrafficByID(id) clientTraffics, err := a.inboundService.GetClientTrafficByID(id)
@ -100,7 +87,6 @@ func (a *InboundController) getClientTrafficsById(c *gin.Context) {
jsonObj(c, clientTraffics, nil) jsonObj(c, clientTraffics, nil)
} }
// addInbound creates a new inbound configuration.
func (a *InboundController) addInbound(c *gin.Context) { func (a *InboundController) addInbound(c *gin.Context) {
inbound := &model.Inbound{} inbound := &model.Inbound{}
err := c.ShouldBind(inbound) err := c.ShouldBind(inbound)
@ -116,7 +102,8 @@ func (a *InboundController) addInbound(c *gin.Context) {
inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port) inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
} }
inbound, needRestart, err := a.inboundService.AddInbound(inbound) needRestart := false
inbound, needRestart, err = a.inboundService.AddInbound(inbound)
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
@ -127,14 +114,14 @@ func (a *InboundController) addInbound(c *gin.Context) {
} }
} }
// delInbound deletes an inbound configuration by its ID.
func (a *InboundController) delInbound(c *gin.Context) { func (a *InboundController) delInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), err)
return return
} }
needRestart, err := a.inboundService.DelInbound(id) needRestart := true
needRestart, err = a.inboundService.DelInbound(id)
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
@ -145,7 +132,6 @@ func (a *InboundController) delInbound(c *gin.Context) {
} }
} }
// updateInbound updates an existing inbound configuration.
func (a *InboundController) updateInbound(c *gin.Context) { func (a *InboundController) updateInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
@ -160,7 +146,8 @@ func (a *InboundController) updateInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
return return
} }
inbound, needRestart, err := a.inboundService.UpdateInbound(inbound) needRestart := true
inbound, needRestart, err = a.inboundService.UpdateInbound(inbound)
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
@ -171,7 +158,6 @@ func (a *InboundController) updateInbound(c *gin.Context) {
} }
} }
// getClientIps retrieves the IP addresses associated with a client by email.
func (a *InboundController) getClientIps(c *gin.Context) { func (a *InboundController) getClientIps(c *gin.Context) {
email := c.Param("email") email := c.Param("email")
@ -184,7 +170,6 @@ func (a *InboundController) getClientIps(c *gin.Context) {
jsonObj(c, ips, nil) jsonObj(c, ips, nil)
} }
// clearClientIps clears the IP addresses for a client by email.
func (a *InboundController) clearClientIps(c *gin.Context) { func (a *InboundController) clearClientIps(c *gin.Context) {
email := c.Param("email") email := c.Param("email")
@ -196,7 +181,6 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.logCleanSuccess"), nil) 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) { func (a *InboundController) addInboundClient(c *gin.Context) {
data := &model.Inbound{} data := &model.Inbound{}
err := c.ShouldBind(data) err := c.ShouldBind(data)
@ -205,7 +189,9 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
return return
} }
needRestart, err := a.inboundService.AddInboundClient(data) needRestart := true
needRestart, err = a.inboundService.AddInboundClient(data)
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
@ -216,7 +202,6 @@ 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) { func (a *InboundController) delInboundClient(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
@ -225,7 +210,9 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
} }
clientId := c.Param("clientId") clientId := c.Param("clientId")
needRestart, err := a.inboundService.DelInboundClient(id, clientId) needRestart := true
needRestart, err = a.inboundService.DelInboundClient(id, clientId)
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
@ -236,7 +223,6 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
} }
} }
// updateInboundClient updates a client's configuration in an inbound.
func (a *InboundController) updateInboundClient(c *gin.Context) { func (a *InboundController) updateInboundClient(c *gin.Context) {
clientId := c.Param("clientId") clientId := c.Param("clientId")
@ -247,7 +233,9 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
return return
} }
needRestart, err := a.inboundService.UpdateInboundClient(inbound, clientId) needRestart := true
needRestart, err = a.inboundService.UpdateInboundClient(inbound, clientId)
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
@ -258,7 +246,6 @@ 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) { func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
@ -278,7 +265,6 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
} }
} }
// resetAllTraffics resets all traffic counters across all inbounds.
func (a *InboundController) resetAllTraffics(c *gin.Context) { func (a *InboundController) resetAllTraffics(c *gin.Context) {
err := a.inboundService.ResetAllTraffics() err := a.inboundService.ResetAllTraffics()
if err != nil { if err != nil {
@ -290,7 +276,6 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllTrafficSuccess"), nil) 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) { func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
@ -308,7 +293,6 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil) jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil)
} }
// importInbound imports an inbound configuration from provided data.
func (a *InboundController) importInbound(c *gin.Context) { func (a *InboundController) importInbound(c *gin.Context) {
inbound := &model.Inbound{} inbound := &model.Inbound{}
err := json.Unmarshal([]byte(c.PostForm("data")), inbound) err := json.Unmarshal([]byte(c.PostForm("data")), inbound)
@ -338,7 +322,6 @@ 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) { func (a *InboundController) delDepletedClients(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
@ -353,18 +336,10 @@ func (a *InboundController) delDepletedClients(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.delDepletedClientsSuccess"), nil) jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.delDepletedClientsSuccess"), nil)
} }
// onlines retrieves the list of currently online clients.
func (a *InboundController) onlines(c *gin.Context) { func (a *InboundController) onlines(c *gin.Context) {
jsonObj(c, a.inboundService.GetOnlineClients(), nil) jsonObj(c, a.inboundService.GetOnlineClients(), nil)
} }
// 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) { func (a *InboundController) updateClientTraffic(c *gin.Context) {
email := c.Param("email") email := c.Param("email")
@ -389,24 +364,3 @@ func (a *InboundController) updateClientTraffic(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil) 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()
}
}

View file

@ -5,22 +5,20 @@ import (
"text/template" "text/template"
"time" "time"
"github.com/mhsanaei/3x-ui/v2/logger" "x-ui/logger"
"github.com/mhsanaei/3x-ui/v2/web/service" "x-ui/web/service"
"github.com/mhsanaei/3x-ui/v2/web/session" "x-ui/web/session"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// LoginForm represents the login request structure.
type LoginForm struct { type LoginForm struct {
Username string `json:"username" form:"username"` Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"` Password string `json:"password" form:"password"`
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"` TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
} }
// IndexController handles the main index and login-related routes.
type IndexController struct { type IndexController struct {
BaseController BaseController
@ -29,23 +27,19 @@ type IndexController struct {
tgbot service.Tgbot tgbot service.Tgbot
} }
// NewIndexController creates a new IndexController and initializes its routes.
func NewIndexController(g *gin.RouterGroup) *IndexController { func NewIndexController(g *gin.RouterGroup) *IndexController {
a := &IndexController{} a := &IndexController{}
a.initRouter(g) a.initRouter(g)
return a return a
} }
// initRouter sets up the routes for index, login, logout, and two-factor authentication.
func (a *IndexController) initRouter(g *gin.RouterGroup) { func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index) g.GET("/", a.index)
g.GET("/logout", a.logout)
g.POST("/login", a.login) g.POST("/login", a.login)
g.GET("/logout", a.logout)
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable) 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) { func (a *IndexController) index(c *gin.Context) {
if session.IsLogin(c) { if session.IsLogin(c) {
c.Redirect(http.StatusTemporaryRedirect, "panel/") c.Redirect(http.StatusTemporaryRedirect, "panel/")
@ -54,7 +48,6 @@ func (a *IndexController) index(c *gin.Context) {
html(c, "login.html", "pages.login.title", nil) html(c, "login.html", "pages.login.title", nil)
} }
// login handles user authentication and session creation.
func (a *IndexController) login(c *gin.Context) { func (a *IndexController) login(c *gin.Context) {
var form LoginForm var form LoginForm
@ -102,7 +95,6 @@ func (a *IndexController) login(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil) 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) { func (a *IndexController) logout(c *gin.Context) {
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
if user != nil { if user != nil {
@ -115,7 +107,6 @@ func (a *IndexController) logout(c *gin.Context) {
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")) c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
} }
// getTwoFactorEnable retrieves the current status of two-factor authentication.
func (a *IndexController) getTwoFactorEnable(c *gin.Context) { func (a *IndexController) getTwoFactorEnable(c *gin.Context) {
status, err := a.settingService.GetTwoFactorEnable() status, err := a.settingService.GetTwoFactorEnable()
if err == nil { if err == nil {

View file

@ -4,52 +4,44 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
"strconv"
"time" "time"
"github.com/mhsanaei/3x-ui/v2/web/global" "x-ui/web/global"
"github.com/mhsanaei/3x-ui/v2/web/service" "x-ui/web/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`) var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
// ServerController handles server management and status-related operations.
type ServerController struct { type ServerController struct {
BaseController BaseController
serverService service.ServerService serverService service.ServerService
settingService service.SettingService settingService service.SettingService
lastStatus *service.Status lastStatus *service.Status
lastGetStatusTime time.Time
lastVersions []string lastVersions []string
lastGetVersionsTime int64 // unix seconds lastGetVersionsTime time.Time
} }
// NewServerController creates a new ServerController, initializes routes, and starts background tasks.
func NewServerController(g *gin.RouterGroup) *ServerController { func NewServerController(g *gin.RouterGroup) *ServerController {
a := &ServerController{} a := &ServerController{
lastGetStatusTime: time.Now(),
}
a.initRouter(g) a.initRouter(g)
a.startTask() a.startTask()
return a return a
} }
// initRouter sets up the routes for server status, Xray management, and utility endpoints.
func (a *ServerController) initRouter(g *gin.RouterGroup) { func (a *ServerController) initRouter(g *gin.RouterGroup) {
g = g.Group("/server")
g.GET("/status", a.status) g.Use(a.checkLogin)
g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket) g.POST("/status", a.status)
g.GET("/getXrayVersion", a.getXrayVersion) g.POST("/getXrayVersion", a.getXrayVersion)
g.GET("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb)
g.GET("/getNewUUID", a.getNewUUID)
g.GET("/getNewX25519Cert", a.getNewX25519Cert)
g.GET("/getNewmldsa65", a.getNewmldsa65)
g.GET("/getNewmlkem768", a.getNewmlkem768)
g.GET("/getNewVlessEnc", a.getNewVlessEnc)
g.POST("/stopXrayService", a.stopXrayService) g.POST("/stopXrayService", a.stopXrayService)
g.POST("/restartXrayService", a.restartXrayService) g.POST("/restartXrayService", a.restartXrayService)
g.POST("/installXray/:version", a.installXray) g.POST("/installXray/:version", a.installXray)
@ -57,61 +49,39 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/updateGeofile/:fileName", a.updateGeofile) g.POST("/updateGeofile/:fileName", a.updateGeofile)
g.POST("/logs/:count", a.getLogs) g.POST("/logs/:count", a.getLogs)
g.POST("/xraylogs/:count", a.getXrayLogs) g.POST("/xraylogs/:count", a.getXrayLogs)
g.POST("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb)
g.POST("/importDB", a.importDB) g.POST("/importDB", a.importDB)
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
g.POST("/getNewmldsa65", a.getNewmldsa65)
g.POST("/getNewEchCert", a.getNewEchCert) g.POST("/getNewEchCert", a.getNewEchCert)
} }
// refreshStatus updates the cached server status and collects CPU history.
func (a *ServerController) refreshStatus() { func (a *ServerController) refreshStatus() {
a.lastStatus = a.serverService.GetStatus(a.lastStatus) 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)
}
} }
// startTask initiates background tasks for continuous status monitoring.
func (a *ServerController) startTask() { func (a *ServerController) startTask() {
webServer := global.GetWebServer() webServer := global.GetWebServer()
c := webServer.GetCron() c := webServer.GetCron()
c.AddFunc("@every 2s", func() { c.AddFunc("@every 2s", func() {
// Always refresh to keep CPU history collected continuously. now := time.Now()
// Sampling is lightweight and capped to ~6 hours in memory. if now.Sub(a.lastGetStatusTime) > time.Minute*3 {
return
}
a.refreshStatus() a.refreshStatus()
}) })
} }
// status returns the current server status information. func (a *ServerController) status(c *gin.Context) {
func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) } a.lastGetStatusTime = time.Now()
// getCpuHistoryBucket retrieves aggregated CPU usage history based on the specified time bucket. jsonObj(c, a.lastStatus, nil)
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) { func (a *ServerController) getXrayVersion(c *gin.Context) {
now := time.Now().Unix() now := time.Now()
if now-a.lastGetVersionsTime <= 60 { // 1 minute cache if now.Sub(a.lastGetVersionsTime) <= time.Minute {
jsonObj(c, a.lastVersions, nil) jsonObj(c, a.lastVersions, nil)
return return
} }
@ -123,35 +93,25 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
} }
a.lastVersions = versions a.lastVersions = versions
a.lastGetVersionsTime = now a.lastGetVersionsTime = time.Now()
jsonObj(c, versions, nil) jsonObj(c, versions, nil)
} }
// installXray installs or updates Xray to the specified version.
func (a *ServerController) installXray(c *gin.Context) { func (a *ServerController) installXray(c *gin.Context) {
version := c.Param("version") version := c.Param("version")
err := a.serverService.UpdateXray(version) err := a.serverService.UpdateXray(version)
jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err) jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err)
} }
// updateGeofile updates the specified geo file for Xray.
func (a *ServerController) updateGeofile(c *gin.Context) { func (a *ServerController) updateGeofile(c *gin.Context) {
fileName := c.Param("fileName") 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) err := a.serverService.UpdateGeofile(fileName)
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err) jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err)
} }
// stopXrayService stops the Xray service.
func (a *ServerController) stopXrayService(c *gin.Context) { func (a *ServerController) stopXrayService(c *gin.Context) {
a.lastGetStatusTime = time.Now()
err := a.serverService.StopXrayService() err := a.serverService.StopXrayService()
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err) jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err)
@ -160,7 +120,6 @@ func (a *ServerController) stopXrayService(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.xray.stopSuccess"), err) jsonMsg(c, I18nWeb(c, "pages.xray.stopSuccess"), err)
} }
// restartXrayService restarts the Xray service.
func (a *ServerController) restartXrayService(c *gin.Context) { func (a *ServerController) restartXrayService(c *gin.Context) {
err := a.serverService.RestartXrayService() err := a.serverService.RestartXrayService()
if err != nil { if err != nil {
@ -170,7 +129,6 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.xray.restartSuccess"), err) jsonMsg(c, I18nWeb(c, "pages.xray.restartSuccess"), err)
} }
// getLogs retrieves the application logs based on count, level, and syslog filters.
func (a *ServerController) getLogs(c *gin.Context) { func (a *ServerController) getLogs(c *gin.Context) {
count := c.Param("count") count := c.Param("count")
level := c.PostForm("level") level := c.PostForm("level")
@ -179,7 +137,6 @@ func (a *ServerController) getLogs(c *gin.Context) {
jsonObj(c, logs, nil) jsonObj(c, logs, nil)
} }
// getXrayLogs retrieves Xray logs with filtering options for direct, blocked, and proxy traffic.
func (a *ServerController) getXrayLogs(c *gin.Context) { func (a *ServerController) getXrayLogs(c *gin.Context) {
count := c.Param("count") count := c.Param("count")
filter := c.PostForm("filter") filter := c.PostForm("filter")
@ -224,7 +181,6 @@ func (a *ServerController) getXrayLogs(c *gin.Context) {
jsonObj(c, logs, nil) jsonObj(c, logs, nil)
} }
// getConfigJson retrieves the Xray configuration as JSON.
func (a *ServerController) getConfigJson(c *gin.Context) { func (a *ServerController) getConfigJson(c *gin.Context) {
configJson, err := a.serverService.GetConfigJson() configJson, err := a.serverService.GetConfigJson()
if err != nil { if err != nil {
@ -234,7 +190,6 @@ func (a *ServerController) getConfigJson(c *gin.Context) {
jsonObj(c, configJson, nil) jsonObj(c, configJson, nil)
} }
// getDb downloads the database file.
func (a *ServerController) getDb(c *gin.Context) { func (a *ServerController) getDb(c *gin.Context) {
db, err := a.serverService.GetDb() db, err := a.serverService.GetDb()
if err != nil { if err != nil {
@ -262,7 +217,6 @@ func isValidFilename(filename string) bool {
return filenameRegex.MatchString(filename) return filenameRegex.MatchString(filename)
} }
// importDB imports a database file and restarts the Xray service.
func (a *ServerController) importDB(c *gin.Context) { func (a *ServerController) importDB(c *gin.Context) {
// Get the file from the request body // Get the file from the request body
file, _, err := c.Request.FormFile("db") file, _, err := c.Request.FormFile("db")
@ -273,7 +227,9 @@ func (a *ServerController) importDB(c *gin.Context) {
defer file.Close() defer file.Close()
// Always restart Xray before return // Always restart Xray before return
defer a.serverService.RestartXrayService() defer a.serverService.RestartXrayService()
// lastGetStatusTime removed; no longer needed defer func() {
a.lastGetStatusTime = time.Now()
}()
// Import it // Import it
err = a.serverService.ImportDB(file) err = a.serverService.ImportDB(file)
if err != nil { if err != nil {
@ -283,7 +239,6 @@ func (a *ServerController) importDB(c *gin.Context) {
jsonObj(c, I18nWeb(c, "pages.index.importDatabaseSuccess"), nil) jsonObj(c, I18nWeb(c, "pages.index.importDatabaseSuccess"), nil)
} }
// getNewX25519Cert generates a new X25519 certificate.
func (a *ServerController) getNewX25519Cert(c *gin.Context) { func (a *ServerController) getNewX25519Cert(c *gin.Context) {
cert, err := a.serverService.GetNewX25519Cert() cert, err := a.serverService.GetNewX25519Cert()
if err != nil { if err != nil {
@ -293,7 +248,6 @@ func (a *ServerController) getNewX25519Cert(c *gin.Context) {
jsonObj(c, cert, nil) jsonObj(c, cert, nil)
} }
// getNewmldsa65 generates a new ML-DSA-65 key.
func (a *ServerController) getNewmldsa65(c *gin.Context) { func (a *ServerController) getNewmldsa65(c *gin.Context) {
cert, err := a.serverService.GetNewmldsa65() cert, err := a.serverService.GetNewmldsa65()
if err != nil { if err != nil {
@ -303,7 +257,6 @@ func (a *ServerController) getNewmldsa65(c *gin.Context) {
jsonObj(c, cert, nil) jsonObj(c, cert, nil)
} }
// getNewEchCert generates a new ECH certificate for the given SNI.
func (a *ServerController) getNewEchCert(c *gin.Context) { func (a *ServerController) getNewEchCert(c *gin.Context) {
sni := c.PostForm("sni") sni := c.PostForm("sni")
cert, err := a.serverService.GetNewEchCert(sni) cert, err := a.serverService.GetNewEchCert(sni)
@ -313,34 +266,3 @@ func (a *ServerController) getNewEchCert(c *gin.Context) {
} }
jsonObj(c, cert, nil) 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)
}

View file

@ -4,15 +4,14 @@ import (
"errors" "errors"
"time" "time"
"github.com/mhsanaei/3x-ui/v2/util/crypto" "x-ui/util/crypto"
"github.com/mhsanaei/3x-ui/v2/web/entity" "x-ui/web/entity"
"github.com/mhsanaei/3x-ui/v2/web/service" "x-ui/web/service"
"github.com/mhsanaei/3x-ui/v2/web/session" "x-ui/web/session"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// updateUserForm represents the form for updating user credentials.
type updateUserForm struct { type updateUserForm struct {
OldUsername string `json:"oldUsername" form:"oldUsername"` OldUsername string `json:"oldUsername" form:"oldUsername"`
OldPassword string `json:"oldPassword" form:"oldPassword"` OldPassword string `json:"oldPassword" form:"oldPassword"`
@ -20,21 +19,18 @@ type updateUserForm struct {
NewPassword string `json:"newPassword" form:"newPassword"` NewPassword string `json:"newPassword" form:"newPassword"`
} }
// SettingController handles settings and user management operations.
type SettingController struct { type SettingController struct {
settingService service.SettingService settingService service.SettingService
userService service.UserService userService service.UserService
panelService service.PanelService panelService service.PanelService
} }
// NewSettingController creates a new SettingController and initializes its routes.
func NewSettingController(g *gin.RouterGroup) *SettingController { func NewSettingController(g *gin.RouterGroup) *SettingController {
a := &SettingController{} a := &SettingController{}
a.initRouter(g) a.initRouter(g)
return a return a
} }
// initRouter sets up the routes for settings management.
func (a *SettingController) initRouter(g *gin.RouterGroup) { func (a *SettingController) initRouter(g *gin.RouterGroup) {
g = g.Group("/setting") g = g.Group("/setting")
@ -46,7 +42,6 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
} }
// getAllSetting retrieves all current settings.
func (a *SettingController) getAllSetting(c *gin.Context) { func (a *SettingController) getAllSetting(c *gin.Context) {
allSetting, err := a.settingService.GetAllSetting() allSetting, err := a.settingService.GetAllSetting()
if err != nil { if err != nil {
@ -56,7 +51,6 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
jsonObj(c, allSetting, nil) jsonObj(c, allSetting, nil)
} }
// getDefaultSettings retrieves the default settings based on the host.
func (a *SettingController) getDefaultSettings(c *gin.Context) { func (a *SettingController) getDefaultSettings(c *gin.Context) {
result, err := a.settingService.GetDefaultSettings(c.Request.Host) result, err := a.settingService.GetDefaultSettings(c.Request.Host)
if err != nil { if err != nil {
@ -66,7 +60,6 @@ func (a *SettingController) getDefaultSettings(c *gin.Context) {
jsonObj(c, result, nil) jsonObj(c, result, nil)
} }
// updateSetting updates all settings with the provided data.
func (a *SettingController) updateSetting(c *gin.Context) { func (a *SettingController) updateSetting(c *gin.Context) {
allSetting := &entity.AllSetting{} allSetting := &entity.AllSetting{}
err := c.ShouldBind(allSetting) err := c.ShouldBind(allSetting)
@ -78,7 +71,6 @@ func (a *SettingController) updateSetting(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) 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) { func (a *SettingController) updateUser(c *gin.Context) {
form := &updateUserForm{} form := &updateUserForm{}
err := c.ShouldBind(form) err := c.ShouldBind(form)
@ -104,13 +96,11 @@ func (a *SettingController) updateUser(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
} }
// restartPanel restarts the panel service after a delay.
func (a *SettingController) restartPanel(c *gin.Context) { func (a *SettingController) restartPanel(c *gin.Context) {
err := a.panelService.RestartPanel(time.Second * 3) err := a.panelService.RestartPanel(time.Second * 3)
jsonMsg(c, I18nWeb(c, "pages.settings.restartPanelSuccess"), err) jsonMsg(c, I18nWeb(c, "pages.settings.restartPanelSuccess"), err)
} }
// getDefaultXrayConfig retrieves the default Xray configuration.
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) { func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig() defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
if err != nil { if err != nil {

View file

@ -5,14 +5,13 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/mhsanaei/3x-ui/v2/config" "x-ui/config"
"github.com/mhsanaei/3x-ui/v2/logger" "x-ui/logger"
"github.com/mhsanaei/3x-ui/v2/web/entity" "x-ui/web/entity"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// getRemoteIp extracts the real IP address from the request headers or remote address.
func getRemoteIp(c *gin.Context) string { func getRemoteIp(c *gin.Context) string {
value := c.GetHeader("X-Real-IP") value := c.GetHeader("X-Real-IP")
if value != "" { if value != "" {
@ -28,17 +27,14 @@ func getRemoteIp(c *gin.Context) string {
return ip return ip
} }
// jsonMsg sends a JSON response with a message and error status.
func jsonMsg(c *gin.Context, msg string, err error) { func jsonMsg(c *gin.Context, msg string, err error) {
jsonMsgObj(c, msg, nil, err) 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) { func jsonObj(c *gin.Context, obj any, err error) {
jsonMsgObj(c, "", obj, err) 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) { func jsonMsgObj(c *gin.Context, msg string, obj any, err error) {
m := entity.Msg{ m := entity.Msg{
Obj: obj, Obj: obj,
@ -56,7 +52,6 @@ func jsonMsgObj(c *gin.Context, msg string, obj any, err error) {
c.JSON(http.StatusOK, m) 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) { func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) {
c.JSON(statusCode, entity.Msg{ c.JSON(statusCode, entity.Msg{
Success: success, Success: success,
@ -64,7 +59,6 @@ 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) { func html(c *gin.Context, name string, title string, data gin.H) {
if data == nil { if data == nil {
data = gin.H{} data = gin.H{}
@ -87,7 +81,6 @@ func html(c *gin.Context, name string, title string, data gin.H) {
c.HTML(http.StatusOK, name, getContext(data)) 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 { func getContext(h gin.H) gin.H {
a := gin.H{ a := gin.H{
"cur_ver": config.GetVersion(), "cur_ver": config.GetVersion(),
@ -98,7 +91,6 @@ func getContext(h gin.H) gin.H {
return a return a
} }
// isAjax checks if the request is an AJAX request.
func isAjax(c *gin.Context) bool { func isAjax(c *gin.Context) bool {
return c.GetHeader("X-Requested-With") == "XMLHttpRequest" return c.GetHeader("X-Requested-With") == "XMLHttpRequest"
} }

View file

@ -1,12 +1,11 @@
package controller package controller
import ( import (
"github.com/mhsanaei/3x-ui/v2/web/service" "x-ui/web/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// XraySettingController handles Xray configuration and settings operations.
type XraySettingController struct { type XraySettingController struct {
XraySettingService service.XraySettingService XraySettingService service.XraySettingService
SettingService service.SettingService SettingService service.SettingService
@ -16,27 +15,24 @@ type XraySettingController struct {
WarpService service.WarpService WarpService service.WarpService
} }
// NewXraySettingController creates a new XraySettingController and initializes its routes.
func NewXraySettingController(g *gin.RouterGroup) *XraySettingController { func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
a := &XraySettingController{} a := &XraySettingController{}
a.initRouter(g) a.initRouter(g)
return a return a
} }
// initRouter sets up the routes for Xray settings management.
func (a *XraySettingController) initRouter(g *gin.RouterGroup) { func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
g = g.Group("/xray") 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("/", a.getXraySetting)
g.POST("/warp/:action", a.warp)
g.POST("/update", a.updateSetting) 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("/resetOutboundsTraffic", a.resetOutboundsTraffic) g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
} }
// getXraySetting retrieves the Xray configuration template and inbound tags.
func (a *XraySettingController) getXraySetting(c *gin.Context) { func (a *XraySettingController) getXraySetting(c *gin.Context) {
xraySetting, err := a.SettingService.GetXrayConfigTemplate() xraySetting, err := a.SettingService.GetXrayConfigTemplate()
if err != nil { if err != nil {
@ -52,14 +48,12 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) {
jsonObj(c, xrayResponse, nil) jsonObj(c, xrayResponse, nil)
} }
// updateSetting updates the Xray configuration settings.
func (a *XraySettingController) updateSetting(c *gin.Context) { func (a *XraySettingController) updateSetting(c *gin.Context) {
xraySetting := c.PostForm("xraySetting") xraySetting := c.PostForm("xraySetting")
err := a.XraySettingService.SaveXraySetting(xraySetting) err := a.XraySettingService.SaveXraySetting(xraySetting)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
} }
// getDefaultXrayConfig retrieves the default Xray configuration.
func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) { func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig() defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig()
if err != nil { if err != nil {
@ -69,12 +63,10 @@ func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
jsonObj(c, defaultJsonConfig, nil) jsonObj(c, defaultJsonConfig, nil)
} }
// getXrayResult retrieves the current Xray service result.
func (a *XraySettingController) getXrayResult(c *gin.Context) { func (a *XraySettingController) getXrayResult(c *gin.Context) {
jsonObj(c, a.XrayService.GetXrayResult(), nil) jsonObj(c, a.XrayService.GetXrayResult(), nil)
} }
// warp handles Warp-related operations based on the action parameter.
func (a *XraySettingController) warp(c *gin.Context) { func (a *XraySettingController) warp(c *gin.Context) {
action := c.Param("action") action := c.Param("action")
var resp string var resp string
@ -98,7 +90,6 @@ func (a *XraySettingController) warp(c *gin.Context) {
jsonObj(c, resp, err) jsonObj(c, resp, err)
} }
// getOutboundsTraffic retrieves the traffic statistics for outbounds.
func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) { func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic() outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
if err != nil { if err != nil {
@ -108,7 +99,6 @@ func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
jsonObj(c, outboundsTraffic, nil) jsonObj(c, outboundsTraffic, nil)
} }
// resetOutboundsTraffic resets the traffic statistics for the specified outbound tag.
func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) { func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
tag := c.PostForm("tag") tag := c.PostForm("tag")
err := a.OutboundService.ResetOutboundTraffic(tag) err := a.OutboundService.ResetOutboundTraffic(tag)

View file

@ -4,22 +4,20 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// XUIController is the main controller for the X-UI panel, managing sub-controllers.
type XUIController struct { type XUIController struct {
BaseController BaseController
inboundController *InboundController
settingController *SettingController settingController *SettingController
xraySettingController *XraySettingController xraySettingController *XraySettingController
} }
// NewXUIController creates a new XUIController and initializes its routes.
func NewXUIController(g *gin.RouterGroup) *XUIController { func NewXUIController(g *gin.RouterGroup) *XUIController {
a := &XUIController{} a := &XUIController{}
a.initRouter(g) a.initRouter(g)
return a return a
} }
// initRouter sets up the main panel routes and initializes sub-controllers.
func (a *XUIController) initRouter(g *gin.RouterGroup) { func (a *XUIController) initRouter(g *gin.RouterGroup) {
g = g.Group("/panel") g = g.Group("/panel")
g.Use(a.checkLogin) g.Use(a.checkLogin)
@ -29,26 +27,23 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
g.GET("/settings", a.settings) g.GET("/settings", a.settings)
g.GET("/xray", a.xraySettings) g.GET("/xray", a.xraySettings)
a.inboundController = NewInboundController(g)
a.settingController = NewSettingController(g) a.settingController = NewSettingController(g)
a.xraySettingController = NewXraySettingController(g) a.xraySettingController = NewXraySettingController(g)
} }
// index renders the main panel index page.
func (a *XUIController) index(c *gin.Context) { func (a *XUIController) index(c *gin.Context) {
html(c, "index.html", "pages.index.title", nil) html(c, "index.html", "pages.index.title", nil)
} }
// inbounds renders the inbounds management page.
func (a *XUIController) inbounds(c *gin.Context) { func (a *XUIController) inbounds(c *gin.Context) {
html(c, "inbounds.html", "pages.inbounds.title", nil) html(c, "inbounds.html", "pages.inbounds.title", nil)
} }
// settings renders the settings management page.
func (a *XUIController) settings(c *gin.Context) { func (a *XUIController) settings(c *gin.Context) {
html(c, "settings.html", "pages.settings.title", nil) html(c, "settings.html", "pages.settings.title", nil)
} }
// xraySettings renders the Xray settings page.
func (a *XUIController) xraySettings(c *gin.Context) { func (a *XUIController) xraySettings(c *gin.Context) {
html(c, "xray.html", "pages.xray.title", nil) html(c, "xray.html", "pages.xray.title", nil)
} }

View file

@ -1,4 +1,3 @@
// Package entity defines data structures and entities used by the web layer of the 3x-ui panel.
package entity package entity
import ( import (
@ -8,100 +7,63 @@ import (
"strings" "strings"
"time" "time"
"github.com/mhsanaei/3x-ui/v2/util/common" "x-ui/util/common"
) )
// Msg represents a standard API response message with success status, message text, and optional data object.
type Msg struct { type Msg struct {
Success bool `json:"success"` // Indicates if the operation was successful Success bool `json:"success"`
Msg string `json:"msg"` // Response message text Msg string `json:"msg"`
Obj any `json:"obj"` // Optional data object Obj any `json:"obj"`
} }
// AllSetting contains all configuration settings for the 3x-ui panel including web server, Telegram bot, and subscription settings.
type AllSetting struct { type AllSetting struct {
// Web server settings WebListen string `json:"webListen" form:"webListen"`
WebListen string `json:"webListen" form:"webListen"` // Web server listen IP address WebDomain string `json:"webDomain" form:"webDomain"`
WebDomain string `json:"webDomain" form:"webDomain"` // Web server domain for domain validation WebPort int `json:"webPort" form:"webPort"`
WebPort int `json:"webPort" form:"webPort"` // Web server port number WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebCertFile string `json:"webCertFile" form:"webCertFile"` // Path to SSL certificate file for web server WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` // Path to SSL private key file for web server WebBasePath string `json:"webBasePath" form:"webBasePath"`
WebBasePath string `json:"webBasePath" form:"webBasePath"` // Base path for web panel URLs SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"` // Session maximum age in minutes PageSize int `json:"pageSize" form:"pageSize"`
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
// UI settings TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
PageSize int `json:"pageSize" form:"pageSize"` // Number of items per page in lists RemarkModel string `json:"remarkModel" form:"remarkModel"`
ExpireDiff int `json:"expireDiff" form:"expireDiff"` // Expiration warning threshold in days TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"` // Traffic warning threshold percentage TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
RemarkModel string `json:"remarkModel" form:"remarkModel"` // Remark model pattern for inbounds TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"`
Datepicker string `json:"datepicker" form:"datepicker"` // Date picker format TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"`
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
// Telegram bot settings TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` // Enable Telegram bot notifications TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
TgBotToken string `json:"tgBotToken" form:"tgBotToken"` // Telegram bot token TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"` // Proxy URL for Telegram bot TgCpu int `json:"tgCpu" form:"tgCpu"`
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"` // Custom API server for Telegram bot TgLang string `json:"tgLang" form:"tgLang"`
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"` // Telegram chat ID for notifications TimeLocation string `json:"timeLocation" form:"timeLocation"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"` // Cron schedule for Telegram notifications TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` // Enable database backup via Telegram TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"` // Send login notifications SubEnable bool `json:"subEnable" form:"subEnable"`
TgCpu int `json:"tgCpu" form:"tgCpu"` // CPU usage threshold for alerts SubTitle string `json:"subTitle" form:"subTitle"`
TgLang string `json:"tgLang" form:"tgLang"` // Telegram bot language SubListen string `json:"subListen" form:"subListen"`
SubPort int `json:"subPort" form:"subPort"`
// Security settings SubPath string `json:"subPath" form:"subPath"`
TimeLocation string `json:"timeLocation" form:"timeLocation"` // Time zone location SubDomain string `json:"subDomain" form:"subDomain"`
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"` // Enable two-factor authentication SubCertFile string `json:"subCertFile" form:"subCertFile"`
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"` // Two-factor authentication token SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
SubUpdates int `json:"subUpdates" form:"subUpdates"`
// Subscription server settings ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"`
SubEnable bool `json:"subEnable" form:"subEnable"` // Enable subscription server ExternalTrafficInformURI string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"`
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"` // Enable JSON subscription endpoint SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
SubTitle string `json:"subTitle" form:"subTitle"` // Subscription title SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
SubListen string `json:"subListen" form:"subListen"` // Subscription server listen IP SubURI string `json:"subURI" form:"subURI"`
SubPort int `json:"subPort" form:"subPort"` // Subscription server port SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
SubPath string `json:"subPath" form:"subPath"` // Base path for subscription URLs SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
SubDomain string `json:"subDomain" form:"subDomain"` // Domain for subscription server validation SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
SubCertFile string `json:"subCertFile" form:"subCertFile"` // SSL certificate file for subscription server SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"`
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"` // SSL private key file for subscription server SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
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"` 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 { func (s *AllSetting) CheckValid() error {
if s.WebListen != "" { if s.WebListen != "" {
ip := net.ParseIP(s.WebListen) ip := net.ParseIP(s.WebListen)

View file

@ -1,4 +1,3 @@
// Package global provides global variables and interfaces for accessing web and subscription servers.
package global package global
import ( import (
@ -13,33 +12,27 @@ var (
subServer SubServer subServer SubServer
) )
// WebServer interface defines methods for accessing the web server instance.
type WebServer interface { type WebServer interface {
GetCron() *cron.Cron // Get the cron scheduler GetCron() *cron.Cron
GetCtx() context.Context // Get the server context GetCtx() context.Context
} }
// SubServer interface defines methods for accessing the subscription server instance.
type SubServer interface { type SubServer interface {
GetCtx() context.Context // Get the server context GetCtx() context.Context
} }
// SetWebServer sets the global web server instance.
func SetWebServer(s WebServer) { func SetWebServer(s WebServer) {
webServer = s webServer = s
} }
// GetWebServer returns the global web server instance.
func GetWebServer() WebServer { func GetWebServer() WebServer {
return webServer return webServer
} }
// SetSubServer sets the global subscription server instance.
func SetSubServer(s SubServer) { func SetSubServer(s SubServer) {
subServer = s subServer = s
} }
// GetSubServer returns the global subscription server instance.
func GetSubServer() SubServer { func GetSubServer() SubServer {
return subServer return subServer
} }

View file

@ -8,21 +8,18 @@ import (
"time" "time"
) )
// HashEntry represents a stored hash entry with its value and timestamp.
type HashEntry struct { type HashEntry struct {
Hash string // MD5 hash string Hash string
Value string // Original value Value string
Timestamp time.Time // Time when the hash was created Timestamp time.Time
} }
// HashStorage provides thread-safe storage for hash-value pairs with expiration.
type HashStorage struct { type HashStorage struct {
sync.RWMutex sync.RWMutex
Data map[string]HashEntry // Map of hash to entry Data map[string]HashEntry
Expiration time.Duration // Expiration duration for entries Expiration time.Duration
} }
// NewHashStorage creates a new HashStorage instance with the specified expiration duration.
func NewHashStorage(expiration time.Duration) *HashStorage { func NewHashStorage(expiration time.Duration) *HashStorage {
return &HashStorage{ return &HashStorage{
Data: make(map[string]HashEntry), Data: make(map[string]HashEntry),
@ -30,7 +27,6 @@ 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 { func (h *HashStorage) SaveHash(query string) string {
h.Lock() h.Lock()
defer h.Unlock() defer h.Unlock()
@ -49,7 +45,6 @@ func (h *HashStorage) SaveHash(query string) string {
return md5HashString return md5HashString
} }
// GetValue retrieves the original value for the given hash, returning true if found.
func (h *HashStorage) GetValue(hash string) (string, bool) { func (h *HashStorage) GetValue(hash string) (string, bool) {
h.RLock() h.RLock()
defer h.RUnlock() defer h.RUnlock()
@ -59,13 +54,11 @@ func (h *HashStorage) GetValue(hash string) (string, bool) {
return entry.Value, exists return entry.Value, exists
} }
// IsMD5 checks if the given string is a valid 32-character MD5 hash.
func (h *HashStorage) IsMD5(hash string) bool { func (h *HashStorage) IsMD5(hash string) bool {
match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash) match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash)
return match return match
} }
// RemoveExpiredHashes removes all hash entries that have exceeded the expiration duration.
func (h *HashStorage) RemoveExpiredHashes() { func (h *HashStorage) RemoveExpiredHashes() {
h.Lock() h.Lock()
defer h.Unlock() defer h.Unlock()
@ -79,7 +72,6 @@ func (h *HashStorage) RemoveExpiredHashes() {
} }
} }
// Reset clears all stored hash entries.
func (h *HashStorage) Reset() { func (h *HashStorage) Reset() {
h.Lock() h.Lock()
defer h.Unlock() defer h.Unlock()

View file

@ -2,21 +2,21 @@
<template slot="actions" slot-scope="text, client, index"> <template slot="actions" slot-scope="text, client, index">
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "qrCode" }}</template> <template slot="title">{{ i18n "qrCode" }}</template>
<a-icon :style="{ fontSize: '22px', marginInlineStart: '14px' }" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon> <a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.client.edit" }}</template> <template slot="title">{{ i18n "pages.client.edit" }}</template>
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon> <a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "info" }}</template> <template slot="title">{{ i18n "info" }}</template>
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon> <a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template> <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-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 slot="icon" type="question-circle-o" :style="{ color: 'var(--color-primary-100)'}"></a-icon>
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon> <a-icon :style="{ fontSize: '24px', cursor: 'pointer' }" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
</a-popconfirm> </a-popconfirm>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
@ -25,7 +25,7 @@
</template> </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-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 slot="icon" type="question-circle-o" :style="{ color: '#e04141' }"></a-icon>
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon> <a-icon :style="{ fontSize: '24px', cursor: 'pointer' }" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
</a-popconfirm> </a-popconfirm>
</a-tooltip> </a-tooltip>
</template> </template>
@ -33,23 +33,18 @@
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch> <a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
</template> </template>
<template slot="online" slot-scope="text, client, index"> <template slot="online" slot-scope="text, client, index">
<a-popover :overlay-class-name="themeSwitcher.currentTheme"> <template v-if="client.enable && isClientOnline(client.email)">
<template slot="content" > <a-tag color="green">{{ i18n "online" }}</a-tag>
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]] </template>
</template> <template v-else>
<template v-if="client.enable && isClientOnline(client.email)"> <a-tag>{{ i18n "offline" }}</a-tag>
<a-tag color="green">{{ i18n "online" }}</a-tag> </template>
</template>
<template v-else>
<a-tag>{{ i18n "offline" }}</a-tag>
</template>
</a-popover>
</template> </template>
<template slot="client" slot-scope="text, client"> <template slot="client" slot-scope="text, client">
<a-space direction="horizontal" :size="2"> <a-space direction="horizontal" :size="2">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<template v-if="isClientDepleted(record, client.email)">{{ i18n "depleted" }}</template> <template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template> <template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template> <template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
</template> </template>
@ -90,7 +85,7 @@
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" /> <a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
</td> </td>
<td class="tr-table-bar" v-else-if="client.totalGB > 0"> <td class="tr-table-bar" v-else-if="client.totalGB > 0">
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" /> <a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
</td> </td>
<td v-else class="infinite-bar tr-table-bar"> <td v-else class="infinite-bar tr-table-bar">
<a-progress :show-info="false" :percent="100"></a-progress> <a-progress :show-info="false" :percent="100"></a-progress>
@ -126,7 +121,7 @@
<tr class="tr-table-box"> <tr class="tr-table-box">
<td class="tr-table-rt"> [[ remainedDays(client.expiryTime) ]] </td> <td class="tr-table-rt"> [[ remainedDays(client.expiryTime) ]] </td>
<td class="infinite-bar tr-table-bar"> <td class="infinite-bar tr-table-bar">
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" /> <a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
</td> </td>
<td class="tr-table-lt">[[ client.reset + "d" ]]</td> <td class="tr-table-lt">[[ client.reset + "d" ]]</td>
</tr> </tr>
@ -213,7 +208,7 @@
</tr> </tr>
</table> </table>
</template> </template>
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" /> <a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
</a-popover> </a-popover>
</td> </td>
<td width="120px" v-else class="infinite-bar"> <td width="120px" v-else class="infinite-bar">
@ -247,7 +242,7 @@
</template> </template>
</span> </span>
</template> </template>
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" /> <a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
</a-popover> </a-popover>
</td> </td>
<td width="60px">[[ client.reset + "d" ]]</td> <td width="60px">[[ client.reset + "d" ]]</td>

View file

@ -28,7 +28,7 @@
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'> <a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input-number v-model.number="inbound.port" :min="1" :max="65535"></a-input-number> <a-input-number v-model.number="inbound.port" :min="1" :max="65531"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
@ -44,31 +44,6 @@
<a-input-number v-model.number="dbInbound.totalGB" :min="0"></a-input-number> <a-input-number v-model.number="dbInbound.totalGB" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<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 v-if="datepicker == 'gregorian'">[[
moment(dbInbound.lastTrafficResetTime).format('YYYY-MM-DD HH:mm:ss') ]]</span>
<span v-else>[[ DateUtil.convertToJalalian(moment(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> <a-form-item>
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
@ -108,14 +83,14 @@
{{template "form/shadowsocks"}} {{template "form/shadowsocks"}}
</template> </template>
<!-- tunnel --> <!-- dokodemo-door -->
<template v-if="inbound.protocol === Protocols.TUNNEL"> <template v-if="inbound.protocol === Protocols.DOKODEMO">
{{template "form/tunnel"}} {{template "form/dokodemo"}}
</template> </template>
<!-- mixed --> <!-- socks -->
<template v-if="inbound.protocol === Protocols.MIXED"> <template v-if="inbound.protocol === Protocols.SOCKS">
{{template "form/mixed"}} {{template "form/socks"}}
</template> </template>
<!-- http --> <!-- http -->

View file

@ -210,7 +210,7 @@
</a-form-item> </a-form-item>
</template> </template>
<!-- VLESS/VMess user settings --> <!-- Vnext (vless/vmess) settings -->
<template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)"> <template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
<a-form-item label='ID'> <a-form-item label='ID'>
<a-input v-model.trim="outbound.settings.id"></a-input> <a-input v-model.trim="outbound.settings.id"></a-input>
@ -226,11 +226,6 @@
</template> </template>
<!-- vless settings --> <!-- vless settings -->
<template v-if="outbound.protocol === Protocols.VLESS">
<a-form-item label='encryption'>
<a-input v-model.trim="outbound.settings.encryption"></a-input>
</a-form-item>
</template>
<template v-if="outbound.canEnableTlsFlow()"> <template v-if="outbound.canEnableTlsFlow()">
<a-form-item label='Flow'> <a-form-item label='Flow'>
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
@ -441,9 +436,6 @@
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option> <a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="ECH Config List">
<a-input v-model.trim="outbound.stream.tls.echConfigList"></a-input>
</a-form-item>
<a-form-item label="Allow Insecure"> <a-form-item label="Allow Insecure">
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch> <a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
</a-form-item> </a-form-item>

View file

@ -1,4 +1,4 @@
{{define "form/tunnel"}} {{define "form/dokodemo"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'> <a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
<a-input v-model.trim="inbound.settings.address"></a-input> <a-input v-model.trim="inbound.settings.address"></a-input>

View file

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

View file

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

View file

@ -12,8 +12,8 @@
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Target'> <a-form-item label='Dest (Target)'>
<a-input v-model.trim="inbound.stream.reality.target"></a-input> <a-input v-model.trim="inbound.stream.reality.dest"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='SNI'> <a-form-item label='SNI'>
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input> <a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
@ -22,10 +22,10 @@
<a-input-number v-model.number="inbound.stream.reality.maxTimediff" :min="0"></a-input-number> <a-input-number v-model.number="inbound.stream.reality.maxTimediff" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='Min Client Ver'> <a-form-item label='Min Client Ver'>
<a-input v-model.trim="inbound.stream.reality.minClientVer" placeholder='25.9.11'></a-input> <a-input v-model.trim="inbound.stream.reality.minClientVer"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Max Client Ver'> <a-form-item label='Max Client Ver'>
<a-input v-model.trim="inbound.stream.reality.maxClientVer" placeholder='25.9.11'></a-input> <a-input v-model.trim="inbound.stream.reality.maxClientVer"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
@ -48,10 +48,7 @@
<a-textarea v-model="inbound.stream.reality.privateKey"></a-textarea> <a-textarea v-model="inbound.stream.reality.privateKey"></a-textarea>
</a-form-item> </a-form-item>
<a-form-item label=" "> <a-form-item label=" ">
<a-space> <a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
<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>
<a-form-item label="mldsa65 Seed"> <a-form-item label="mldsa65 Seed">
<a-textarea v-model="inbound.stream.reality.mldsa65Seed"></a-textarea> <a-textarea v-model="inbound.stream.reality.mldsa65Seed"></a-textarea>
@ -60,10 +57,7 @@
<a-textarea v-model="inbound.stream.reality.settings.mldsa65Verify"></a-textarea> <a-textarea v-model="inbound.stream.reality.settings.mldsa65Verify"></a-textarea>
</a-form-item> </a-form-item>
<a-form-item label=" "> <a-form-item label=" ">
<a-space> <a-button type="primary" icon="import" @click="getNewmldsa65">Get New Seed</a-button>
<a-button type="primary" icon="import" @click="getNewmldsa65">Get New Seed</a-button>
<a-button danger @click="clearMldsa65">Clear</a-button>
</a-space>
</a-form-item> </a-form-item>
</template> </template>
{{end}} {{end}}

View file

@ -3,14 +3,12 @@
<a-divider :style="{ margin: '5px 0 0' }"></a-divider> <a-divider :style="{ margin: '5px 0 0' }"></a-divider>
<a-form-item label="External Proxy"> <a-form-item label="External Proxy">
<a-switch v-model="externalProxy"></a-switch> <a-switch v-model="externalProxy"></a-switch>
<a-button icon="plus" v-if="externalProxy" type="primary" :style="{ marginLeft: '10px' }" size="small" <a-button icon="plus" v-if="externalProxy" type="primary" :style="{ marginLeft: '10px' }" size="small" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"></a-button>
@click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"></a-button>
</a-form-item> </a-form-item>
<a-input-group :style="{ margin: '8px 0' }" compact v-for="(row, index) in inbound.stream.externalProxy"> <a-input-group :style="{ margin: '8px 0' }" compact v-for="(row, index) in inbound.stream.externalProxy">
<template> <template>
<a-tooltip title="Force TLS"> <a-tooltip title="Force TLS">
<a-select v-model="row.forceTls" :style="{ width: '20%', margin: '0px' }" <a-select v-model="row.forceTls" :style="{ width: '20%', margin: '0px' }" :dropdown-class-name="themeSwitcher.currentTheme">
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option> <a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option>
<a-select-option value="none">{{ i18n "none" }}</a-select-option> <a-select-option value="none">{{ i18n "none" }}</a-select-option>
<a-select-option value="tls">TLS</a-select-option> <a-select-option value="tls">TLS</a-select-option>
@ -19,7 +17,7 @@
</template> </template>
<a-input :style="{ width: '30%' }" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input> <a-input :style="{ width: '30%' }" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input>
<a-tooltip title='{{ i18n "pages.inbounds.port" }}'> <a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
<a-input-number :style="{ width: '15%' }" v-model.number="row.port" min="1" max="65535"></a-input-number> <a-input-number :style="{ width: '15%' }" v-model.number="row.port" min="1" max="65531"></a-input-number>
</a-tooltip> </a-tooltip>
<a-input :style="{ width: '30%', top: '0' }" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'> <a-input :style="{ width: '30%', top: '0' }" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
<template slot="addonAfter"> <template slot="addonAfter">

View file

@ -116,10 +116,7 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label=" "> <a-form-item label=" ">
<a-space>
<a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button> <a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button>
<a-button danger @click="clearEchCert">Clear</a-button>
</a-space>
</a-form-item> </a-form-item>
</template> </template>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

@ -121,7 +121,7 @@
}, },
methods: { methods: {
async getDBClientIps(email) { async getDBClientIps(email) {
const msg = await HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`); const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`);
if (!msg.success) { if (!msg.success) {
document.getElementById("clientIPs").value = msg.obj; document.getElementById("clientIPs").value = msg.obj;
return; return;
@ -139,7 +139,7 @@
}, },
async clearDBClientIps(email) { async clearDBClientIps(email) {
try { try {
const msg = await HttpUtil.post(`/panel/api/inbounds/clearClientIps/${email}`); const msg = await HttpUtil.post(`/panel/inbound/clearClientIps/${email}`);
if (!msg.success) { if (!msg.success) {
return; return;
} }
@ -156,7 +156,7 @@
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
iconElement.disabled = true; iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + email); const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email);
if (msg.success) { if (msg.success) {
this.clientModal.clientStats.up = 0; this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0; this.clientModal.clientStats.down = 0;

View file

@ -3,29 +3,22 @@
:mask-closable="false" :footer="null" :class="themeSwitcher.currentTheme"> :mask-closable="false" :footer="null" :class="themeSwitcher.currentTheme">
<a-list class="ant-dns-presets-list" bordered :style="{ width: '100%' }"> <a-list class="ant-dns-presets-list" bordered :style="{ width: '100%' }">
<a-list-item v-for="dns in dnsPresetsDatabase" :style="{ padding: '12px 16px' }"> <a-list-item v-for="dns in dnsPresetsDatabase" :style="{ padding: '12px 16px' }">
<div class="ant-dns-presets-line"> <a-row justify="space-between" align="middle">
<a-space direction="horizontal" size="small" align="center"> <a-col :span="12">
<a-tag :color="dns.family ? 'purple' : 'green'">[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}' : 'DNS' ]]</a-tag> <a-space direction="vertical" size="small">
<span class="ant-dns-presets-list-name">[[ dns.name ]]</span> <span class="ant-dns-presets-list-name">[[ dns.name ]]</span>
</a-space> <a-tag :color="dns.family ? 'purple' : 'green'">[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}' : 'DNS' ]]</a-tag>
<a-button class="ant-dns-presets-install" type="primary" @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button> </a-space>
</div> </a-col>
<a-col :span="12" :style="{ textAlign: 'right' }">
<a-button type="primary" @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
</a-col>
</a-row>
</a-list-item> </a-list-item>
</a-list> </a-list>
</a-modal> </a-modal>
<style> <style>
.ant-dns-presets-line {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.ant-dns-presets-install {
margin-left: auto;
}
.dark .ant-dns-presets-list { .dark .ant-dns-presets-list {
border-color: var(--dark-color-stroke) border-color: var(--dark-color-stroke)
} }

View file

@ -101,19 +101,9 @@
{{ i18n "security" }} {{ i18n "security" }}
<a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag> <a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag>
<br /> <br />
<td>Authentication</td>
<a-tag v-if="inbound.settings.selectedAuth" color="green">[[ inbound.settings.selectedAuth ? inbound.settings.selectedAuth : '' ]]</a-tag>
<a-tag v-else color="red">{{ i18n "none" }}</a-tag>
<br />
{{ i18n "encryption" }}
<a-tag class="info-large-tag" :color="inbound.settings.encryption ? 'green' : 'red'">[[ inbound.settings.encryption ? inbound.settings.encryption : '' ]]</a-tag>
<a-tooltip title='{{ i18n "copy" }}'>
<a-button size="small" icon="snippets" @click="copy(inbound.settings.encryption)"></a-button>
</a-tooltip>
<br />
<template v-if="inbound.stream.security != 'none'"> <template v-if="inbound.stream.security != 'none'">
{{ i18n "domainName" }} {{ i18n "domainName" }}
<a-tag v-if="inbound.serverName" color="green">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> <a-tag v-if="inbound.serverName" :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
<a-tag v-else color="orange">{{ i18n "none" }}</a-tag> <a-tag v-else color="orange">{{ i18n "none" }}</a-tag>
</template> </template>
</template> </template>
@ -183,9 +173,9 @@
<tr> <tr>
<td>{{ i18n "status" }}</td> <td>{{ i18n "status" }}</td>
<td> <td>
<a-tag v-if="isDepleted" color="red">{{ i18n "depleted" }}</a-tag> <a-tag v-if="isEnable" color="green">{{ i18n "enabled" }}</a-tag>
<a-tag v-else-if="isEnable" color="green">{{ i18n "enabled" }}</a-tag>
<a-tag v-else>{{ i18n "disabled" }}</a-tag> <a-tag v-else>{{ i18n "disabled" }}</a-tag>
<a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
</td> </td>
</tr> </tr>
<tr v-if="infoModal.clientStats"> <tr v-if="infoModal.clientStats">
@ -195,44 +185,6 @@
<a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up) ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag> <a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up) ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
</td> </td>
</tr> </tr>
<tr>
<td>{{ i18n "pages.inbounds.createdAt" }}</td>
<td>
<template v-if="infoModal.clientSettings && infoModal.clientSettings.created_at">
<template v-if="app.datepicker === 'gregorian'">
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.created_at) ]]</a-tag>
</template>
<template v-else>
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.created_at)) ]]</a-tag>
</template>
</template>
<template v-else>
<a-tag>-</a-tag>
</template>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.updatedAt" }}</td>
<td>
<template v-if="infoModal.clientSettings && infoModal.clientSettings.updated_at">
<template v-if="app.datepicker === 'gregorian'">
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.updated_at) ]]</a-tag>
</template>
<template v-else>
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.updated_at)) ]]</a-tag>
</template>
</template>
<template v-else>
<a-tag>-</a-tag>
</template>
</td>
</tr>
<tr>
<td>{{ i18n "lastOnline" }}</td>
<td>
<a-tag>[[ app.formatLastOnline(infoModal.clientSettings && infoModal.clientSettings.email ? infoModal.clientSettings.email : '') ]]</a-tag>
</td>
</tr>
<tr v-if="infoModal.clientSettings.comment"> <tr v-if="infoModal.clientSettings.comment">
<td>{{ i18n "comment" }}</td> <td>{{ i18n "comment" }}</td>
<td> <td>
@ -311,7 +263,7 @@
</tr-info-title> </tr-info-title>
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a> <a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
</tr-info-row> </tr-info-row>
<tr-info-row class="tr-info-row" v-if="app.subSettings.subJsonEnable"> <tr-info-row class="tr-info-row">
<tr-info-title class="tr-info-title"> <tr-info-title class="tr-info-title">
<a-tag color="purple">Json Link</a-tag> <a-tag color="purple">Json Link</a-tag>
<a-tooltip title='{{ i18n "copy" }}'> <a-tooltip title='{{ i18n "copy" }}'>
@ -358,7 +310,7 @@
<code>[[ link.link ]]</code> <code>[[ link.link ]]</code>
</tr-info-row> </tr-info-row>
</template> </template>
<table v-if="inbound.protocol == Protocols.TUNNEL" class="tr-info-table"> <table v-if="inbound.protocol == Protocols.DOKODEMO" class="tr-info-table">
<tr> <tr>
<th>{{ i18n "pages.inbounds.targetAddress" }}</th> <th>{{ i18n "pages.inbounds.targetAddress" }}</th>
<th>{{ i18n "pages.inbounds.destinationPort" }}</th> <th>{{ i18n "pages.inbounds.destinationPort" }}</th>
@ -380,7 +332,7 @@
</td> </td>
</tr> </tr>
</table> </table>
<table v-if="dbInbound.isMixed" class="tr-info-table"> <table v-if="dbInbound.isSocks" class="tr-info-table">
<tr> <tr>
<th>{{ i18n "password" }} Auth</th> <th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th> <th>{{ i18n "pages.inbounds.enable" }} udp</th>
@ -496,7 +448,7 @@
</a-modal> </a-modal>
<script> <script>
function refreshIPs(email) { function refreshIPs(email) {
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => { return HttpUtil.post(`/panel/inbound/clientIps/${email}`).then((msg) => {
if (msg.success) { if (msg.success) {
try { try {
return JSON.parse(msg.obj).join(', '); return JSON.parse(msg.obj).join(', ');
@ -527,7 +479,7 @@
this.dbInbound = new DBInbound(dbInbound); this.dbInbound = new DBInbound(dbInbound);
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null; this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry; this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;
this.clientStats = this.inbound.clients ? (this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) || null) : null; this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
if ( if (
[ [
@ -551,7 +503,7 @@
if (this.clientSettings) { if (this.clientSettings) {
if (this.clientSettings.subId) { if (this.clientSettings.subId) {
this.subLink = this.genSubLink(this.clientSettings.subId); this.subLink = this.genSubLink(this.clientSettings.subId);
this.subJsonLink = app.subSettings.subJsonEnable ? this.genSubJsonLink(this.clientSettings.subId) : ''; this.subJsonLink = this.genSubJsonLink(this.clientSettings.subId);
} }
} }
this.visible = true; this.visible = true;
@ -590,24 +542,6 @@
} }
return infoModal.dbInbound.isEnable; return infoModal.dbInbound.isEnable;
}, },
get isDepleted() {
const stats = infoModal.clientStats;
const settings = infoModal.clientSettings;
if (!stats || !settings) {
return false;
}
const total = stats.total ?? 0;
const used = (stats.up ?? 0) + (stats.down ?? 0);
const hasTotal = total > 0;
const exhausted = hasTotal && used >= total;
const expiryTime = settings.expiryTime ?? 0;
const hasExpiry = expiryTime > 0;
const now = Date.now();
const expired = hasExpiry && now >= expiryTime;
return expired || exhausted;
},
}, },
methods: { methods: {
copy(content) { copy(content) {
@ -635,7 +569,7 @@
}); });
}, },
clearClientIps() { clearClientIps() {
HttpUtil.post(`/panel/api/inbounds/clearClientIps/${this.infoModal.clientStats.email}`) HttpUtil.post(`/panel/inbound/clearClientIps/${this.infoModal.clientStats.email}`)
.then((msg) => { .then((msg) => {
if (!msg.success) { if (!msg.success) {
return; return;

View file

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

View file

@ -30,7 +30,7 @@
</tr-qr-bg-inner> </tr-qr-bg-inner>
</tr-qr-bg> </tr-qr-bg>
</tr-qr-box> </tr-qr-box>
<tr-qr-box class="qr-box" v-if="app.subSettings.subJsonEnable"> <tr-qr-box class="qr-box">
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}} Json</span></a-tag> <a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}} Json</span></a-tag>
<tr-qr-bg class="qr-bg-sub"> <tr-qr-bg class="qr-bg-sub">
<tr-qr-bg-inner class="qr-bg-sub-inner"> <tr-qr-bg-inner class="qr-bg-sub-inner">
@ -151,7 +151,7 @@
methods: { methods: {
async getStatus() { async getStatus() {
try { try {
const msg = await HttpUtil.get('/panel/api/server/status'); const msg = await HttpUtil.post('/server/status');
if (msg.success) { if (msg.success) {
this.serverStatus = msg.obj; this.serverStatus = msg.obj;
} }
@ -262,9 +262,7 @@
if (qrModal.client && qrModal.client.subId) { if (qrModal.client && qrModal.client.subId) {
qrModal.subId = qrModal.client.subId; qrModal.subId = qrModal.client.subId;
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId)); this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
if (app.subSettings.subJsonEnable) { this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
}
} }
qrModal.qrcodes.forEach((element, index) => { qrModal.qrcodes.forEach((element, index) => {
this.setQrCode("qrCode-" + index, element.link); this.setQrCode("qrCode-" + index, element.link);

View file

@ -7,13 +7,12 @@
<a-input v-model.trim="dnsModal.dnsServer.address"></a-input> <a-input v-model.trim="dnsModal.dnsServer.address"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'> <a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input-number v-model.number="dnsModal.dnsServer.port" :min="1" :max="65535"></a-input-number> <a-input-number v-model.number="dnsModal.dnsServer.port" :min="1" :max="65531"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.strategy" }}'> <a-form-item label='{{ i18n "pages.xray.dns.strategy" }}'>
<a-select v-model="dnsModal.dnsServer.queryStrategy" :style="{ width: '100%' }" <a-select v-model="dnsModal.dnsServer.queryStrategy" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']"> [[ l ]] <a-select-option :value="l" :label="l" v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']"> [[ l ]] </a-select-option>
</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-divider :style="{ margin: '5px 0' }"></a-divider> <a-divider :style="{ margin: '5px 0' }"></a-divider>

View file

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

View file

@ -1,28 +1,86 @@
{{ template "page/head_start" .}} {{ template "page/head_start" .}}
<style>
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
@media (max-width: 768px) {
.ant-tabs-nav .ant-tabs-tab {
margin: 0;
padding: 12px .5rem;
}
}
.ant-tabs-bar {
margin: 0;
}
.ant-list-item {
display: block;
}
.alert-msg {
color: rgb(194, 117, 18);
font-weight: normal;
font-size: 16px;
padding: .5rem 1rem;
text-align: center;
background: rgb(255 145 0 / 15%);
margin: 1.5rem 2.5rem 0rem;
border-radius: .5rem;
transition: all 0.5s;
animation: signal 3s cubic-bezier(0.18, 0.89, 0.32, 1.28) infinite;
}
.alert-msg:hover {
cursor: default;
transition-duration: .3s;
animation: signal 0.9s ease infinite;
}
@keyframes signal {
0% {
box-shadow: 0 0 0 0 rgba(194, 118, 18, 0.5);
}
50% {
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
}
100% {
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
}
}
.alert-msg>i {
color: inherit;
font-size: 24px;
}
.dark .ant-input-password-icon {
color: var(--dark-color-text-primary);
}
.ant-collapse-content-box .ant-alert {
margin-block-end: 12px;
}
</style>
{{ template "page/head_end" .}} {{ template "page/head_end" .}}
{{ template "page/body_start" .}} {{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' settings-page'"> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<a-sidebar></a-sidebar> <a-sidebar></a-sidebar>
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content> <a-layout-content>
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'> <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear> <transition name="list" appear>
<a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched" :style="{ marginBottom: '10px' }" <a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched" :style="{ marginBottom: '10px' }"
message='{{ i18n "secAlertTitle" }}' color="red" show-icon closable> message='{{ i18n "secAlertTitle" }}'
color="red"
show-icon closable>
<template slot="description"> <template slot="description">
<b>{{ i18n "secAlertConf" }}</b> <b>{{ i18n "secAlertConf" }}</b>
<ul> <ul><li v-for="a in confAlerts">[[ a ]]</li></ul>
<li v-for="a in confAlerts">[[ a ]]</li>
</ul>
</template> </template>
</a-alert> </a-alert>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<template> <template>
<a-row v-if="!loadingStates.fetched"> <a-row v-if="!loadingStates.fetched">
<a-card <a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
:style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
<a-spin tip='{{ i18n "loading" }}'></a-spin> <a-spin tip='{{ i18n "loading" }}'></a-spin>
</a-card> </a-card>
</a-row> </a-row>
@ -32,19 +90,17 @@
<a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }"> <a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }"> <a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
<a-space direction="horizontal"> <a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
"pages.settings.save" }}</a-button> <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n
"pages.settings.restartPanel" }}</a-button>
</a-space> </a-space>
</a-col> </a-col>
<a-col :xs="24" :sm="14"> <a-col :xs="24" :sm="14">
<template> <template>
<div> <div>
<a-back-top :target="() => document.getElementById('content-layout')" <a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
visibility-height="200"></a-back-top>
<a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }" <a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
message='{{ i18n "pages.settings.infoDesc" }}' show-icon> message='{{ i18n "pages.settings.infoDesc" }}'
show-icon>
</a-alert> </a-alert>
</div> </div>
</template> </template>
@ -82,7 +138,7 @@
</template> </template>
{{ template "settings/panel/subscription/general" . }} {{ template "settings/panel/subscription/general" . }}
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="5" v-if="allSetting.subJsonEnable" :style="{ paddingTop: '20px' }"> <a-tab-pane key="5" v-if="allSetting.subEnable" :style="{ paddingTop: '20px' }">
<template #tab> <template #tab>
<a-icon type="code"></a-icon> <a-icon type="code"></a-icon>
<span>{{ i18n "pages.settings.subSettings" }} (JSON)</span> <span>{{ i18n "pages.settings.subSettings" }} (JSON)</span>
@ -122,7 +178,6 @@
saveBtnDisable: true, saveBtnDisable: true,
user: {}, user: {},
lang: LanguageManager.getLanguage(), lang: LanguageManager.getLanguage(),
inboundOptions: [],
remarkModels: { i: 'Inbound', e: 'Email', o: 'Other' }, remarkModels: { i: 'Inbound', e: 'Email', o: 'Other' },
remarkSeparators: [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'], remarkSeparators: [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'],
datepickerList: [{ name: 'Gregorian (Standard)', value: 'gregorian' }, { name: 'Jalalian (شمسی)', value: 'jalalian' }], datepickerList: [{ name: 'Gregorian (Standard)', value: 'gregorian' }, { name: 'Jalalian (شمسی)', value: 'jalalian' }],
@ -135,8 +190,7 @@
fragment: { fragment: {
packets: "tlshello", packets: "tlshello",
length: "100-200", length: "100-200",
interval: "10-20", interval: "10-20"
maxSplit: "300-400"
} }
}, },
streamSettings: { streamSettings: {
@ -247,17 +301,6 @@
this.saveBtnDisable = true; this.saveBtnDisable = true;
} }
}, },
async loadInboundTags() {
const msg = await HttpUtil.get("/panel/api/inbounds/list");
if (msg && msg.success && Array.isArray(msg.obj)) {
this.inboundOptions = msg.obj.map(ib => ({
label: `${ib.tag} (${ib.protocol}@${ib.port})`,
value: ib.tag,
}));
} else {
this.inboundOptions = [];
}
},
async updateAllSetting() { async updateAllSetting() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post("/panel/setting/update", this.allSetting); const msg = await HttpUtil.post("/panel/setting/update", this.allSetting);
@ -384,15 +427,6 @@
}, },
}, },
computed: { computed: {
ldapInboundTagList: {
get: function () {
const csv = this.allSetting.ldapInboundTags || "";
return csv.length ? csv.split(',').map(s => s.trim()).filter(Boolean) : [];
},
set: function (list) {
this.allSetting.ldapInboundTags = Array.isArray(list) ? list.join(',') : '';
}
},
fragment: { fragment: {
get: function () { return this.allSetting?.subJsonFragment != ""; }, get: function () { return this.allSetting?.subJsonFragment != ""; },
set: function (v) { set: function (v) {
@ -429,16 +463,6 @@
} }
} }
}, },
fragmentMaxSplit: {
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.maxSplit : ""; },
set: function (v) {
if (v != "") {
newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.settings.fragment.maxSplit = v;
this.allSetting.subJsonFragment = JSON.stringify(newFragment);
}
}
},
noises: { noises: {
get() { get() {
return this.allSetting?.subJsonNoises != ""; return this.allSetting?.subJsonNoises != "";
@ -558,8 +582,6 @@
if (this.allSetting.subEnable) { if (this.allSetting.subEnable) {
subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath; subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath;
if (subPath == '/sub/') alerts.push('{{ i18n "secAlertSubURI" }}'); if (subPath == '/sub/') alerts.push('{{ i18n "secAlertSubURI" }}');
}
if (this.allSetting.subJsonEnable) {
subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath; subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;
if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}'); if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}');
} }
@ -569,7 +591,7 @@
}, },
async mounted() { async mounted() {
await this.getAllSetting(); await this.getAllSetting();
await this.loadInboundTags();
while (true) { while (true) {
await PromiseUtil.sleep(1000); await PromiseUtil.sleep(1000);
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting); this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);

View file

@ -39,7 +39,7 @@
<template #title>{{ i18n "pages.settings.panelPort"}}</template> <template #title>{{ i18n "pages.settings.panelPort"}}</template>
<template #description>{{ i18n "pages.settings.panelPortDesc"}}</template> <template #description>{{ i18n "pages.settings.panelPortDesc"}}</template>
<template #control> <template #control>
<a-input-number :min="1" :min="65535" v-model="allSetting.webPort" :style="{ width: '100%' }"></a-input> <a-input-number :min="1" :min="65531" v-model="allSetting.webPort" :style="{ width: '100%' }"></a-input>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
@ -137,8 +137,7 @@
<template #title>{{ i18n "pages.settings.datepicker"}}</template> <template #title>{{ i18n "pages.settings.datepicker"}}</template>
<template #description>{{ i18n "pages.settings.datepickerDescription"}}</template> <template #description>{{ i18n "pages.settings.datepickerDescription"}}</template>
<template #control> <template #control>
<a-select :style="{ width: '100%' }" :dropdown-class-name="themeSwitcher.currentTheme" <a-select :style="{ width: '100%' }" :dropdown-class-name="themeSwitcher.currentTheme" v-model="datepicker">
v-model="datepicker">
<a-select-option v-for="item in datepickerList" :value="item.value"> <a-select-option v-for="item in datepickerList" :value="item.value">
<span v-text="item.name"></span> <span v-text="item.name"></span>
</a-select-option> </a-select-option>
@ -146,135 +145,5 @@
</template> </template>
</a-setting-list-item> </a-setting-list-item>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel key="6" header='LDAP'>
<a-setting-list-item paddings="small">
<template #title>Enable LDAP sync</template>
<template #control>
<a-switch v-model="allSetting.ldapEnable"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>LDAP Host</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapHost"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>LDAP Port</template>
<template #control>
<a-input-number :min="1" :max="65535" v-model="allSetting.ldapPort" :style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Use TLS (LDAPS)</template>
<template #control>
<a-switch v-model="allSetting.ldapUseTLS"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Bind DN</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapBindDN"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Password</template>
<template #control>
<a-input type="password" v-model="allSetting.ldapPassword"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Base DN</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapBaseDN"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>User filter</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapUserFilter"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>User attribute (username/email)</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapUserAttr"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>VLESS flag attribute</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapVlessField"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Generic flag attribute (optional)</template>
<template #description>If set, overrides VLESS flag; e.g. shadowInactive</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapFlagField"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Truthy values</template>
<template #description>Comma-separated; default: true,1,yes,on</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapTruthyValues"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Invert flag</template>
<template #description>Enable when attribute means disabled (e.g., shadowInactive)</template>
<template #control>
<a-switch v-model="allSetting.ldapInvertFlag"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Sync schedule</template>
<template #description>cron-like string, e.g. @every 1m</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapSyncCron"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Inbound tags</template>
<template #description>Select inbounds to manage (auto create/delete)</template>
<template #control>
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }" v-model="ldapInboundTagList">
<a-select-option v-for="opt in inboundOptions" :key="opt.value" :value="opt.value">[[ opt.label ]]</a-select-option>
</a-select>
<div v-if="inboundOptions.length==0" style="margin-top:6px;color:#999">No inbounds found. Please create one in Inbounds.</div>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Auto create clients</template>
<template #control>
<a-switch v-model="allSetting.ldapAutoCreate"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Auto delete clients</template>
<template #control>
<a-switch v-model="allSetting.ldapAutoDelete"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Default total (GB)</template>
<template #control>
<a-input-number :min="0" v-model="allSetting.ldapDefaultTotalGB" :style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Default expiry (days)</template>
<template #control>
<a-input-number :min="0" v-model="allSetting.ldapDefaultExpiryDays" :style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Default Limit IP</template>
<template #control>
<a-input-number :min="0" v-model="allSetting.ldapDefaultLimitIP" :style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
</a-collapse-panel>
</a-collapse> </a-collapse>
{{end}} {{end}}

View file

@ -8,13 +8,6 @@
<a-switch v-model="allSetting.subEnable"></a-switch> <a-switch v-model="allSetting.subEnable"></a-switch>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>JSON Subscription</template>
<template #description>{{ i18n "pages.settings.subJsonEnable"}}</template>
<template #control>
<a-switch v-model="allSetting.subJsonEnable"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.subTitle"}}</template> <template #title>{{ i18n "pages.settings.subTitle"}}</template>
<template #description>{{ i18n "pages.settings.subTitleDesc"}}</template> <template #description>{{ i18n "pages.settings.subTitleDesc"}}</template>
@ -40,7 +33,7 @@
<template #title>{{ i18n "pages.settings.subPort"}}</template> <template #title>{{ i18n "pages.settings.subPort"}}</template>
<template #description>{{ i18n "pages.settings.subPortDesc"}}</template> <template #description>{{ i18n "pages.settings.subPortDesc"}}</template>
<template #control> <template #control>
<a-input-number v-model="allSetting.subPort" :min="1" :min="65535" <a-input-number v-model="allSetting.subPort" :min="1" :min="65531"
:style="{ width: '100%' }"></a-input-number> :style="{ width: '100%' }"></a-input-number>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
@ -48,10 +41,7 @@
<template #title>{{ i18n "pages.settings.subPath"}}</template> <template #title>{{ i18n "pages.settings.subPath"}}</template>
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template> <template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
<template #control> <template #control>
<a-input type="text" v-model="allSetting.subPath" <a-input type="text" v-model="allSetting.subPath"></a-input>
@input="allSetting.subPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')"
@blur="allSetting.subPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subPath)"
placeholder="/sub/"></a-input>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">

View file

@ -5,10 +5,7 @@
<template #title>{{ i18n "pages.settings.subPath"}}</template> <template #title>{{ i18n "pages.settings.subPath"}}</template>
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template> <template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
<template #control> <template #control>
<a-input type="text" v-model="allSetting.subJsonPath" <a-input type="text" v-model="allSetting.subJsonPath"></a-input>
@input="allSetting.subJsonPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')"
@blur="allSetting.subJsonPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subJsonPath)"
placeholder="/json/"></a-input>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
@ -50,12 +47,6 @@
<a-input type="text" v-model="fragmentInterval" placeholder="10-20"></a-input> <a-input type="text" v-model="fragmentInterval" placeholder="10-20"></a-input>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>MaxSplit</template>
<template #control>
<a-input type="text" v-model="fragmentMaxSplit" placeholder="300-400"></a-input>
</template>
</a-setting-list-item>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</a-list-item> </a-list-item>
@ -77,8 +68,7 @@
<a-select :value="noise.type" :style="{ width: '100%' }" <a-select :value="noise.type" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme" :dropdown-class-name="themeSwitcher.currentTheme"
@change="(value) => updateNoiseType(index, value)"> @change="(value) => updateNoiseType(index, value)">
<a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str', 'hex']" <a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str', 'hex']" :key="p">
:key="p">
<span>[[ p ]]</span> <span>[[ p ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>

View file

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

View file

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

View file

@ -3,23 +3,57 @@
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
<style>
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
@media (max-width: 768px) {
.ant-tabs-nav .ant-tabs-tab {
margin: 0;
padding: 12px .5rem;
}
.ant-table-thead>tr>th,
.ant-table-tbody>tr>td {
padding: 10px 0px;
}
}
.ant-tabs-bar {
margin: 0;
}
.ant-list-item {
display: block;
}
.ant-list-item>li {
padding: 10px 20px !important;
}
.ant-collapse-content-box .ant-alert {
margin-block-end: 12px;
}
</style>
{{ template "page/head_end" .}} {{ template "page/head_end" .}}
{{ template "page/body_start" .}} {{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' xray-page'"> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<a-sidebar></a-sidebar> <a-sidebar></a-sidebar>
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content> <a-layout-content>
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'> <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear> <transition name="list" appear>
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }" <a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }" message='{{ i18n "secAlertTitle" }}'
message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable> color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
</a-alert> </a-alert>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-row v-if="!loadingStates.fetched"> <a-row v-if="!loadingStates.fetched">
<a-card <a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
:style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
<a-spin tip='{{ i18n "loading" }}'></a-spin> <a-spin tip='{{ i18n "loading" }}'></a-spin>
</a-card> </a-card>
</a-row> </a-row>
@ -38,8 +72,7 @@
<a-popover v-if="restartResult" :overlay-class-name="themeSwitcher.currentTheme"> <a-popover v-if="restartResult" :overlay-class-name="themeSwitcher.currentTheme">
<span slot="title">{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span> <span slot="title">{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
<template slot="content"> <template slot="content">
<span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line <span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line ]]</span>
]]</span>
</template> </template>
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-popover> </a-popover>
@ -148,9 +181,8 @@
{ title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } }, { title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } },
{ {
title: '{{ i18n "pages.xray.rules.source"}}', children: [ title: '{{ i18n "pages.xray.rules.source"}}', children: [
{ title: 'IP', dataIndex: "sourceIP", align: 'center', width: 20, ellipsis: true }, { title: 'IP', dataIndex: "source", align: 'center', width: 20, ellipsis: true },
{ title: '{{ i18n "pages.inbounds.port" }}', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true }, { title: '{{ i18n "pages.inbounds.port" }}', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true }]
{ title: 'VLESS Route', dataIndex: 'vlessRoute', align: 'center', width: 15, ellipsis: true }]
}, },
{ {
title: '{{ i18n "pages.inbounds.network"}}', children: [ title: '{{ i18n "pages.inbounds.network"}}', children: [
@ -319,7 +351,7 @@
{ label: '🇨🇳 China', value: 'geosite:cn' }, { label: '🇨🇳 China', value: 'geosite:cn' },
{ label: '🇨🇳 .cn', value: 'regexp:.*\\.cn$' }, { label: '🇨🇳 .cn', value: 'regexp:.*\\.cn$' },
{ label: '🇷🇺 Russia', value: 'ext:geosite_RU.dat:ru-available-only-inside' }, { label: '🇷🇺 Russia', value: 'ext:geosite_RU.dat:ru-available-only-inside' },
{ label: '🇷🇺 .ru', value: 'regexp:.*\\.ru$' }, { label: '🇷🇺 .ru', value: 'regexp:.*\\.ru' },
{ label: '🇷🇺 .su', value: 'regexp:.*\\.su$' }, { label: '🇷🇺 .su', value: 'regexp:.*\\.su$' },
{ label: '🇷🇺 .рф', value: 'regexp:.*\\.xn--p1ai$' }, { label: '🇷🇺 .рф', value: 'regexp:.*\\.xn--p1ai$' },
{ label: '🇻🇳 .vn', value: 'regexp:.*\\.vn$' }, { label: '🇻🇳 .vn', value: 'regexp:.*\\.vn$' },
@ -388,7 +420,7 @@
}, },
async restartXray() { async restartXray() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post("/panel/api/server/restartXrayService"); const msg = await HttpUtil.post("server/restartXrayService");
this.loading(false); this.loading(false);
if (msg.success) { if (msg.success) {
await PromiseUtil.sleep(500); await PromiseUtil.sleep(500);
@ -536,10 +568,9 @@
serverObj = null; serverObj = null;
switch (o.protocol) { switch (o.protocol) {
case Protocols.VMess: case Protocols.VMess:
case Protocols.VLESS:
serverObj = o.settings.vnext; serverObj = o.settings.vnext;
break; break;
case Protocols.VLESS:
return [o.settings?.address + ':' + o.settings?.port];
case Protocols.HTTP: case Protocols.HTTP:
case Protocols.Socks: case Protocols.Socks:
case Protocols.Shadowsocks: case Protocols.Shadowsocks:

View file

@ -8,17 +8,15 @@ import (
"os" "os"
"os/exec" "os/exec"
"regexp" "regexp"
"runtime"
"sort" "sort"
"time" "time"
"github.com/mhsanaei/3x-ui/v2/database" "x-ui/database"
"github.com/mhsanaei/3x-ui/v2/database/model" "x-ui/database/model"
"github.com/mhsanaei/3x-ui/v2/logger" "x-ui/logger"
"github.com/mhsanaei/3x-ui/v2/xray" "x-ui/xray"
) )
// CheckClientIpJob monitors client IP addresses from access logs and manages IP blocking based on configured limits.
type CheckClientIpJob struct { type CheckClientIpJob struct {
lastClear int64 lastClear int64
disAllowedIps []string disAllowedIps []string
@ -26,7 +24,6 @@ type CheckClientIpJob struct {
var job *CheckClientIpJob var job *CheckClientIpJob
// NewCheckClientIpJob creates a new client IP monitoring job instance.
func NewCheckClientIpJob() *CheckClientIpJob { func NewCheckClientIpJob() *CheckClientIpJob {
job = new(CheckClientIpJob) job = new(CheckClientIpJob)
return job return job
@ -42,20 +39,12 @@ func (j *CheckClientIpJob) Run() {
f2bInstalled := j.checkFail2BanInstalled() f2bInstalled := j.checkFail2BanInstalled()
isAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive) isAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive)
if isAccessLogAvailable { if iplimitActive {
if runtime.GOOS == "windows" { if f2bInstalled && isAccessLogAvailable {
if iplimitActive { shouldClearAccessLog = j.processLogFile()
shouldClearAccessLog = j.processLogFile()
}
} else { } else {
if iplimitActive { if !f2bInstalled {
if f2bInstalled { logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.")
shouldClearAccessLog = j.processLogFile()
} else {
if !f2bInstalled {
logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.")
}
}
} }
} }
} }

View file

@ -4,23 +4,21 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/mhsanaei/3x-ui/v2/web/service" "x-ui/web/service"
"github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/cpu"
) )
// CheckCpuJob monitors CPU usage and sends Telegram notifications when usage exceeds the configured threshold.
type CheckCpuJob struct { type CheckCpuJob struct {
tgbotService service.Tgbot tgbotService service.Tgbot
settingService service.SettingService settingService service.SettingService
} }
// NewCheckCpuJob creates a new CPU monitoring job instance.
func NewCheckCpuJob() *CheckCpuJob { func NewCheckCpuJob() *CheckCpuJob {
return new(CheckCpuJob) return new(CheckCpuJob)
} }
// Run checks CPU usage over the last minute and sends a Telegram alert if it exceeds the threshold. // Here run is a interface method of Job interface
func (j *CheckCpuJob) Run() { func (j *CheckCpuJob) Run() {
threshold, _ := j.settingService.GetTgCpu() threshold, _ := j.settingService.GetTgCpu()

View file

@ -1,20 +1,18 @@
package job package job
import ( import (
"github.com/mhsanaei/3x-ui/v2/web/service" "x-ui/web/service"
) )
// CheckHashStorageJob periodically cleans up expired hash entries from the Telegram bot's hash storage.
type CheckHashStorageJob struct { type CheckHashStorageJob struct {
tgbotService service.Tgbot tgbotService service.Tgbot
} }
// NewCheckHashStorageJob creates a new hash storage cleanup job instance.
func NewCheckHashStorageJob() *CheckHashStorageJob { func NewCheckHashStorageJob() *CheckHashStorageJob {
return new(CheckHashStorageJob) return new(CheckHashStorageJob)
} }
// Run removes expired hash entries from the Telegram bot's hash storage. // Here Run is an interface method of the Job interface
func (j *CheckHashStorageJob) Run() { func (j *CheckHashStorageJob) Run() {
// Remove expired hashes from storage // Remove expired hashes from storage
j.tgbotService.GetHashStorage().RemoveExpiredHashes() j.tgbotService.GetHashStorage().RemoveExpiredHashes()

View file

@ -1,24 +1,20 @@
// Package job provides background job implementations for the 3x-ui web panel,
// including traffic monitoring, system checks, and periodic maintenance tasks.
package job package job
import ( import (
"github.com/mhsanaei/3x-ui/v2/logger" "x-ui/logger"
"github.com/mhsanaei/3x-ui/v2/web/service" "x-ui/web/service"
) )
// CheckXrayRunningJob monitors Xray process health and restarts it if it crashes.
type CheckXrayRunningJob struct { type CheckXrayRunningJob struct {
xrayService service.XrayService xrayService service.XrayService
checkTime int
checkTime int
} }
// NewCheckXrayRunningJob creates a new Xray health check job instance.
func NewCheckXrayRunningJob() *CheckXrayRunningJob { func NewCheckXrayRunningJob() *CheckXrayRunningJob {
return new(CheckXrayRunningJob) return new(CheckXrayRunningJob)
} }
// Run checks if Xray has crashed and restarts it after confirming it's down for 2 consecutive checks.
func (j *CheckXrayRunningJob) Run() { func (j *CheckXrayRunningJob) Run() {
if !j.xrayService.DidXrayCrash() { if !j.xrayService.DidXrayCrash() {
j.checkTime = 0 j.checkTime = 0

View file

@ -5,14 +5,12 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/mhsanaei/3x-ui/v2/logger" "x-ui/logger"
"github.com/mhsanaei/3x-ui/v2/xray" "x-ui/xray"
) )
// ClearLogsJob clears old log files to prevent disk space issues.
type ClearLogsJob struct{} type ClearLogsJob struct{}
// NewClearLogsJob creates a new log cleanup job instance.
func NewClearLogsJob() *ClearLogsJob { func NewClearLogsJob() *ClearLogsJob {
return new(ClearLogsJob) return new(ClearLogsJob)
} }

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