mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-03-20 09:35:48 +00:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01d4a7488d | ||
|
|
2b2ed3349a | ||
|
|
d8523bbdac | ||
|
|
8afa39144e | ||
|
|
00baeffe74 | ||
|
|
b578a33518 | ||
|
|
8153e0ac05 | ||
|
|
2eb9d2e2e8 | ||
|
|
a824875c4f | ||
|
|
cafcb250ec | ||
|
|
e7cfee570b | ||
|
|
90c3529301 | ||
|
|
b65ec83c39 | ||
|
|
28a17a80ec | ||
|
|
3056583388 | ||
|
|
172f2ddaa7 | ||
|
|
d69af328dc | ||
|
|
ee0e3093ba | ||
|
|
89def9aee6 | ||
|
|
b2b0024648 | ||
|
|
5822758b7c | ||
|
|
49430b3991 | ||
|
|
104526aab2 | ||
|
|
a0c07241c0 | ||
|
|
adf3242602 | ||
|
|
3f62592e4b | ||
|
|
02bff4db6c | ||
|
|
8ff4e1ff31 | ||
|
|
26c6438ec2 | ||
|
|
b3e96230c4 | ||
|
|
1016f3b4f9 | ||
|
|
020bc9d77c | ||
|
|
5620d739c6 | ||
|
|
d518979e4f | ||
|
|
83f8a03b50 | ||
|
|
b45e63a14a | ||
|
|
3007bcff97 | ||
|
|
55f1d72af5 | ||
|
|
806ecbd7c5 | ||
|
|
ae79b43cdb | ||
|
|
e64e6327ef | ||
|
|
9f024b9e6a | ||
|
|
eacfbc86b5 | ||
|
|
37c17357fc | ||
|
|
b35d339665 | ||
|
|
5e7a3db873 | ||
|
|
6ced549dea | ||
|
|
f60682a6b7 | ||
|
|
50bd7a8040 | ||
|
|
7465768ff7 | ||
|
|
5b00a52c65 | ||
|
|
151f1173a1 | ||
|
|
e262132b9d | ||
|
|
ca0a7aeb5a | ||
|
|
7447cec17e |
85
.github/workflows/docker.yml
vendored
85
.github/workflows/docker.yml
vendored
@@ -1,4 +1,9 @@
|
|||||||
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:
|
||||||
@@ -10,48 +15,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
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
hsanaeii/3x-ui
|
|
||||||
ghcr.io/mhsanaei/3x-ui
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=tag
|
|
||||||
type=pep440,pattern={{version}}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Docker meta
|
||||||
uses: docker/setup-qemu-action@v3
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
hsanaeii/3x-ui
|
||||||
|
ghcr.io/mhsanaei/3x-ui
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up QEMU
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
|
||||||
install: true
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Set up Docker Buildx
|
||||||
uses: docker/login-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
install: true
|
||||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
username: ${{ github.repository_owner }}
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Login to GHCR
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
registry: ghcr.io
|
||||||
push: true
|
username: ${{ github.actor }}
|
||||||
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -85,7 +85,7 @@ jobs:
|
|||||||
cd x-ui/bin
|
cd x-ui/bin
|
||||||
|
|
||||||
# Download dependencies
|
# Download dependencies
|
||||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.9.11/"
|
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.10.15/"
|
||||||
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
|
||||||
@@ -183,7 +183,7 @@ jobs:
|
|||||||
cd x-ui\bin
|
cd x-ui\bin
|
||||||
|
|
||||||
# Download Xray for Windows
|
# Download Xray for Windows
|
||||||
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.9.11/"
|
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.10.15/"
|
||||||
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
|
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
|
||||||
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
|
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
|
||||||
Remove-Item "Xray-windows-64.zip"
|
Remove-Item "Xray-windows-64.zip"
|
||||||
|
|||||||
51
.vscode/tasks.json
vendored
51
.vscode/tasks.json
vendored
@@ -5,36 +5,71 @@
|
|||||||
"label": "go: build",
|
"label": "go: build",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "go",
|
"command": "go",
|
||||||
"args": ["build", "-o", "bin/3x-ui.exe", "./main.go"],
|
"args": [
|
||||||
|
"build",
|
||||||
|
"-o",
|
||||||
|
"bin/3x-ui.exe",
|
||||||
|
"./main.go"
|
||||||
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
"problemMatcher": ["$go"],
|
"problemMatcher": [
|
||||||
"group": { "kind": "build", "isDefault": true }
|
"$go"
|
||||||
|
],
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "go: run",
|
"label": "go: run",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "go",
|
"command": "go",
|
||||||
"args": ["run", "./main.go"],
|
"args": [
|
||||||
|
"run",
|
||||||
|
"./main.go"
|
||||||
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"env": {
|
"env": {
|
||||||
"XUI_DEBUG": "true"
|
"XUI_DEBUG": "true"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"problemMatcher": ["$go"]
|
"problemMatcher": [
|
||||||
|
"$go"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "go: test",
|
"label": "go: test",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "go",
|
"command": "go",
|
||||||
"args": ["test", "./..."],
|
"args": [
|
||||||
|
"test",
|
||||||
|
"./..."
|
||||||
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
"problemMatcher": ["$go"],
|
"problemMatcher": [
|
||||||
|
"$go"
|
||||||
|
],
|
||||||
"group": "test"
|
"group": "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "go: vet",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go",
|
||||||
|
"args": [
|
||||||
|
"vet",
|
||||||
|
"./..."
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"problemMatcher": [
|
||||||
|
"$go"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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.10.15/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}"
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ 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" ]
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
</picture>
|
</picture>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||||
[](#)
|
[](#)
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
|
||||||
**3X-UI** — لوحة تحكم متقدمة مفتوحة المصدر تعتمد على الويب مصممة لإدارة خادم Xray-core. توفر واجهة سهلة الاستخدام لتكوين ومراقبة بروتوكولات VPN والوكيل المختلفة.
|
**3X-UI** — لوحة تحكم متقدمة مفتوحة المصدر تعتمد على الويب مصممة لإدارة خادم Xray-core. توفر واجهة سهلة الاستخدام لتكوين ومراقبة بروتوكولات VPN والوكيل المختلفة.
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
</picture>
|
</picture>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||||
[](#)
|
[](#)
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
|
||||||
**3X-UI** — panel de control avanzado basado en web de código abierto diseñado para gestionar el servidor Xray-core. Ofrece una interfaz fácil de usar para configurar y monitorear varios protocolos VPN y proxy.
|
**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.
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
</picture>
|
</picture>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||||
[](#)
|
[](#)
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
|
||||||
**3X-UI** — یک پنل کنترل پیشرفته مبتنی بر وب با کد باز که برای مدیریت سرور Xray-core طراحی شده است. این پنل یک رابط کاربری آسان برای پیکربندی و نظارت بر پروتکلهای مختلف VPN و پراکسی ارائه میدهد.
|
**3X-UI** — یک پنل کنترل پیشرفته مبتنی بر وب با کد باز که برای مدیریت سرور Xray-core طراحی شده است. این پنل یک رابط کاربری آسان برای پیکربندی و نظارت بر پروتکلهای مختلف VPN و پراکسی ارائه میدهد.
|
||||||
|
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -7,11 +7,13 @@
|
|||||||
</picture>
|
</picture>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||||
[](#)
|
[](#)
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
|
||||||
**3X-UI** — advanced, open-source web-based control panel designed for managing Xray-core server. It offers a user-friendly interface for configuring and monitoring various VPN and proxy protocols.
|
**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.
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
</picture>
|
</picture>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||||
[](#)
|
[](#)
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
|
||||||
**3X-UI** — продвинутая панель управления с открытым исходным кодом на основе веб-интерфейса, разработанная для управления сервером Xray-core. Предоставляет удобный интерфейс для настройки и мониторинга различных VPN и прокси-протоколов.
|
**3X-UI** — продвинутая панель управления с открытым исходным кодом на основе веб-интерфейса, разработанная для управления сервером Xray-core. Предоставляет удобный интерфейс для настройки и мониторинга различных VPN и прокси-протоколов.
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
</picture>
|
</picture>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||||
[](#)
|
[](#)
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
|
||||||
**3X-UI** — 一个基于网页的高级开源控制面板,专为管理 Xray-core 服务器而设计。它提供了用户友好的界面,用于配置和监控各种 VPN 和代理协议。
|
**3X-UI** — 一个基于网页的高级开源控制面板,专为管理 Xray-core 服务器而设计。它提供了用户友好的界面,用于配置和监控各种 VPN 和代理协议。
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// 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 (
|
||||||
@@ -16,24 +18,29 @@ 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"
|
||||||
Warn LogLevel = "warn"
|
Warning LogLevel = "warning"
|
||||||
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
|
||||||
@@ -45,10 +52,12 @@ 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 == "" {
|
||||||
@@ -74,6 +83,7 @@ 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 != "" {
|
||||||
@@ -85,10 +95,12 @@ 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 != "" {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.8.2
|
2.8.5
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
// Package database provides database initialization, migration, and management utilities
|
||||||
|
// for the 3x-ui panel using GORM with SQLite.
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
@@ -9,10 +12,10 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"github.com/mhsanaei/3x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/util/crypto"
|
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||||
"github.com/mhsanaei/3x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -45,6 +48,7 @@ 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 {
|
||||||
@@ -68,6 +72,7 @@ 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 {
|
||||||
@@ -107,12 +112,14 @@ 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)
|
||||||
@@ -151,6 +158,7 @@ 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()
|
||||||
@@ -162,14 +170,17 @@ 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))
|
||||||
@@ -180,6 +191,7 @@ 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
|
||||||
@@ -188,3 +200,29 @@ func Checkpoint() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateSQLiteDB opens the provided sqlite DB path with a throw-away connection
|
||||||
|
// and runs a PRAGMA integrity_check to ensure the file is structurally sound.
|
||||||
|
// It does not mutate global state or run migrations.
|
||||||
|
func ValidateSQLiteDB(dbPath string) error {
|
||||||
|
if _, err := os.Stat(dbPath); err != nil { // file must exist
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gdb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Discard})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sqlDB, err := gdb.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sqlDB.Close()
|
||||||
|
var res string
|
||||||
|
if err := gdb.Raw("PRAGMA integrity_check;").Scan(&res).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if res != "ok" {
|
||||||
|
return errors.New("sqlite integrity check failed: " + res)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
|
// 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/util/json_util"
|
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||||
"github.com/mhsanaei/3x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/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"
|
||||||
@@ -20,27 +23,29 @@ const (
|
|||||||
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"`
|
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier
|
||||||
UserId int `json:"-"`
|
UserId int `json:"-"` // Associated user ID
|
||||||
Up int64 `json:"up" form:"up"`
|
Up int64 `json:"up" form:"up"` // Upload traffic in bytes
|
||||||
Down int64 `json:"down" form:"down"`
|
Down int64 `json:"down" form:"down"` // Download traffic in bytes
|
||||||
Total int64 `json:"total" form:"total"`
|
Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
|
||||||
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"`
|
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` // All-time traffic usage
|
||||||
Remark string `json:"remark" form:"remark"`
|
Remark string `json:"remark" form:"remark"` // Human-readable remark
|
||||||
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"`
|
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled
|
||||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
|
||||||
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2"`
|
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2"` // Traffic reset schedule
|
||||||
LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"`
|
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"`
|
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics
|
||||||
|
|
||||||
// config part
|
// Xray configuration fields
|
||||||
Listen string `json:"listen" form:"listen"`
|
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"`
|
||||||
@@ -50,6 +55,7 @@ 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"`
|
||||||
@@ -58,17 +64,20 @@ 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 != "" {
|
||||||
@@ -85,33 +94,28 @@ 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"`
|
ID string `json:"id"` // Unique client identifier
|
||||||
Security string `json:"security"`
|
Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm")
|
||||||
Password string `json:"password"`
|
Password string `json:"password"` // Client password
|
||||||
Flow string `json:"flow"`
|
Flow string `json:"flow"` // Flow control (XTLS)
|
||||||
Email string `json:"email"`
|
Email string `json:"email"` // Client email identifier
|
||||||
LimitIP int `json:"limitIp"`
|
LimitIP int `json:"limitIp"` // IP limit for this client
|
||||||
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB
|
||||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
|
||||||
Enable bool `json:"enable" form:"enable"`
|
Enable bool `json:"enable" form:"enable"` // Whether the client is enabled
|
||||||
TgID int64 `json:"tgId" form:"tgId"`
|
TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications
|
||||||
SubID string `json:"subId" form:"subId"`
|
SubID string `json:"subId" form:"subId"` // Subscription identifier
|
||||||
Comment string `json:"comment" form:"comment"`
|
Comment string `json:"comment" form:"comment"` // Client comment
|
||||||
Reset int `json:"reset" form:"reset"`
|
Reset int `json:"reset" form:"reset"` // Reset period in days
|
||||||
CreatedAt int64 `json:"created_at,omitempty"`
|
CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
|
||||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
|
||||||
}
|
|
||||||
|
|
||||||
type VLESSSettings struct {
|
|
||||||
Clients []Client `json:"clients"`
|
|
||||||
Decryption string `json:"decryption"`
|
|
||||||
Encryption string `json:"encryption"`
|
|
||||||
Fallbacks []any `json:"fallbacks"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
58
go.mod
58
go.mod
@@ -1,11 +1,12 @@
|
|||||||
module github.com/mhsanaei/3x-ui
|
module github.com/mhsanaei/3x-ui/v2
|
||||||
|
|
||||||
go 1.25.1
|
go 1.25.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-contrib/gzip v1.2.3
|
github.com/gin-contrib/gzip v1.2.4
|
||||||
github.com/gin-contrib/sessions v1.0.4
|
github.com/gin-contrib/sessions v1.0.4
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.11.0
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.12
|
||||||
github.com/goccy/go-json v0.10.5
|
github.com/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
|
||||||
@@ -14,21 +15,22 @@ 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.8
|
github.com/shirou/gopsutil/v4 v4.25.9
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/valyala/fasthttp v1.65.0
|
github.com/valyala/fasthttp v1.67.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.250911.1-0.20251015080723-b69a376aa1b6
|
||||||
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.43.0
|
||||||
golang.org/x/sys v0.36.0
|
golang.org/x/sys v0.37.0
|
||||||
golang.org/x/text v0.29.0
|
golang.org/x/text v0.30.0
|
||||||
google.golang.org/grpc v1.75.1
|
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.30.5
|
gorm.io/gorm v1.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
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/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic v1.14.1 // indirect
|
github.com/bytedance/sonic v1.14.1 // indirect
|
||||||
@@ -36,13 +38,15 @@ require (
|
|||||||
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.8.4 // indirect
|
github.com/ebitengine/purego v0.9.0 // 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.27.0 // indirect
|
github.com/go-playground/validator/v10 v10.28.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
|
||||||
@@ -57,7 +61,7 @@ require (
|
|||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||||
github.com/miekg/dns v1.1.68 // indirect
|
github.com/miekg/dns v1.1.68 // indirect
|
||||||
@@ -66,11 +70,11 @@ 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.54.0 // indirect
|
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||||
github.com/refraction-networking/utls v1.8.0 // indirect
|
github.com/refraction-networking/utls v1.8.1 // 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.7 // indirect
|
github.com/sagernet/sing v0.7.12 // indirect
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
github.com/sagernet/sing-shadowsocks v0.2.9 // 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
|
||||||
@@ -82,21 +86,19 @@ 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-20250904214705-431b6ff8c67c // indirect
|
github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 // 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.22.0 // indirect
|
||||||
golang.org/x/mod v0.28.0 // indirect
|
golang.org/x/mod v0.29.0 // indirect
|
||||||
golang.org/x/net v0.44.0 // indirect
|
golang.org/x/net v0.46.0 // indirect
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
golang.org/x/time v0.13.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
golang.org/x/tools v0.36.0 // indirect
|
golang.org/x/tools v0.38.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-20250908214217-97024824d090 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.10 // 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
|
||||||
)
|
)
|
||||||
|
|||||||
120
go.sum
120
go.sum
@@ -1,5 +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/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
@@ -19,20 +23,24 @@ 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.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.9.0/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=
|
||||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||||
github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U=
|
github.com/gin-contrib/gzip v1.2.4 h1:yNz4EhPC2kHSZJD1oc1zwp7MLEhEZ3goQeGM3a1b6jU=
|
||||||
github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c=
|
github.com/gin-contrib/gzip v1.2.4/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw=
|
||||||
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
|
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
|
||||||
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
|
github.com/gin-contrib/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.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 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=
|
||||||
@@ -46,10 +54,12 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/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.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||||
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=
|
||||||
@@ -73,6 +83,20 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
|||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/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=
|
||||||
@@ -93,8 +117,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
@@ -124,24 +148,24 @@ 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.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||||
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
|
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
|
||||||
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/sagernet/sing v0.7.7 h1:o46FzVZS+wKbBMEkMEdEHoVZxyM9jvfRpKXc7pEgS/c=
|
github.com/sagernet/sing v0.7.12 h1:MpMbO56crPRZTbltoj1wGk4Xj9+GiwH1wTO4s3fz1EA=
|
||||||
github.com/sagernet/sing v0.7.7/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
github.com/sagernet/sing v0.7.12/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
||||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 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.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
|
github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
|
github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
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/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=
|
||||||
@@ -166,8 +190,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.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
|
github.com/valyala/fasthttp v1.67.0 h1:tqKlJMUP6iuNG8hGjK/s9J4kadH7HLV4ijEcPGsezac=
|
||||||
github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
|
github.com/valyala/fasthttp v1.67.0/go.mod h1:qYSIpqt/0XNmShgo/8Aq8E3UYWVVwNS2QYmzd8WIEPM=
|
||||||
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=
|
||||||
@@ -176,10 +200,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-20250904214705-431b6ff8c67c h1:LHLhQY3mKXSpTcQAkjFR4/6ar3rXjQryNeM7khK3AHU=
|
github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 h1:nwobseOLLRtdbP6z7Z2aVI97u8ZptTgD1ofovhAKmeU=
|
||||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
|
github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ=
|
||||||
github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno=
|
github.com/xtls/xray-core v1.250911.1-0.20251015080723-b69a376aa1b6 h1:gwgJxWb9OABUJAYxiS33nQzk3MRVjidzBnHBrzKnxOw=
|
||||||
github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k=
|
github.com/xtls/xray-core v1.250911.1-0.20251015080723-b69a376aa1b6/go.mod h1:72ZU/srfutsNPmw9y8SCGRy0iccvshIRk8BNGR8D2Ik=
|
||||||
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=
|
||||||
@@ -202,14 +226,14 @@ 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.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -218,26 +242,26 @@ 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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||||
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-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f h1:1FTH6cpXFsENbPR5Bu8NQddPSaUUE6NA2XdZdDSAJK4=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
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=
|
||||||
@@ -249,8 +273,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.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s=
|
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||||
gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
|
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c 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=
|
||||||
|
|||||||
49
install.sh
49
install.sh
@@ -53,9 +53,12 @@ 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-tumbleweed | opensuse-leap)
|
||||||
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
|
||||||
;;
|
;;
|
||||||
@@ -146,11 +149,15 @@ 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 "${red}Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later${plain}"
|
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
|
||||||
exit 1
|
tag_version=$(curl -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||||
|
if [[ ! -n "$tag_version" ]]; then
|
||||||
|
echo -e "${red}Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later${plain}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
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 -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 --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
|
||||||
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
|
||||||
@@ -167,17 +174,25 @@ 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 -N -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
|
wget --inet4-only -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 -O /usr/bin/x-ui-temp https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
|
wget --inet4-only -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
|
||||||
systemctl stop x-ui
|
if [[ $release == "alpine" ]]; then
|
||||||
|
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
|
||||||
|
|
||||||
@@ -201,10 +216,22 @@ install_x-ui() {
|
|||||||
chmod +x /usr/bin/x-ui
|
chmod +x /usr/bin/x-ui
|
||||||
config_after_install
|
config_after_install
|
||||||
|
|
||||||
cp -f x-ui.service /etc/systemd/system/
|
if [[ $release == "alpine" ]]; then
|
||||||
systemctl daemon-reload
|
wget --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
|
||||||
systemctl enable x-ui
|
if [[ $? -ne 0 ]]; then
|
||||||
systemctl start x-ui
|
echo -e "${red}Failed to download x-ui.rc${plain}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
chmod +x /etc/init.d/x-ui
|
||||||
|
rc-update add x-ui
|
||||||
|
rc-service x-ui start
|
||||||
|
else
|
||||||
|
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 "┌───────────────────────────────────────────────────────┐
|
||||||
|
|||||||
138
logger/logger.go
138
logger/logger.go
@@ -1,15 +1,29 @@
|
|||||||
|
// Package logger provides logging functionality for the 3x-ui panel with
|
||||||
|
// dual-backend logging (console/syslog and file) and buffered log storage for web UI.
|
||||||
package logger
|
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
|
||||||
@@ -17,89 +31,164 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
// InitLogger initializes dual logging backends: console/syslog and file.
|
||||||
InitLogger(logging.INFO)
|
// Console logging uses the specified level, file logging always uses DEBUG level.
|
||||||
}
|
|
||||||
|
|
||||||
func InitLogger(level logging.Level) {
|
func InitLogger(level logging.Level) {
|
||||||
newLogger := logging.MustGetLogger("x-ui")
|
newLogger := logging.MustGetLogger("x-ui")
|
||||||
var err error
|
backends := make([]logging.Backend, 0, 2)
|
||||||
var backend logging.Backend
|
|
||||||
var format logging.Formatter
|
|
||||||
ppid := os.Getppid()
|
|
||||||
|
|
||||||
backend, err = logging.NewSyslogBackend("")
|
// Console/syslog backend with configurable level
|
||||||
if err != nil {
|
if consoleBackend := initDefaultBackend(); consoleBackend != nil {
|
||||||
println(err)
|
leveledBackend := logging.AddModuleLevel(consoleBackend)
|
||||||
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
leveledBackend.SetLevel(level, "x-ui")
|
||||||
}
|
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}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
backendFormatter := logging.NewBackendFormatter(backend, format)
|
// File backend with DEBUG level for comprehensive logging
|
||||||
backendLeveled := logging.AddModuleLevel(backendFormatter)
|
if fileBackend := initFileBackend(); fileBackend != nil {
|
||||||
backendLeveled.SetLevel(level, "x-ui")
|
leveledBackend := logging.AddModuleLevel(fileBackend)
|
||||||
newLogger.SetBackend(backendLeveled)
|
leveledBackend.SetLevel(logging.DEBUG, "x-ui")
|
||||||
|
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) >= 10240 {
|
if len(logBuffer) >= maxLogBufferSize {
|
||||||
logBuffer = logBuffer[1:]
|
logBuffer = logBuffer[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,12 +198,13 @@ func addToBuffer(level string, newLog string) {
|
|||||||
level logging.Level
|
level logging.Level
|
||||||
log string
|
log string
|
||||||
}{
|
}{
|
||||||
time: t.Format("2006/01/02 15:04:05"),
|
time: t.Format(timeFormat),
|
||||||
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
32
main.go
@@ -1,3 +1,5 @@
|
|||||||
|
// Package main is the entry point for the 3x-ui web panel application.
|
||||||
|
// It initializes the database, web server, and handles command-line operations for managing the panel.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -9,19 +11,20 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
_ "unsafe"
|
_ "unsafe"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"github.com/mhsanaei/3x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/sub"
|
"github.com/mhsanaei/3x-ui/v2/sub"
|
||||||
"github.com/mhsanaei/3x-ui/util/crypto"
|
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||||
"github.com/mhsanaei/3x-ui/web"
|
"github.com/mhsanaei/3x-ui/v2/web"
|
||||||
"github.com/mhsanaei/3x-ui/web/global"
|
"github.com/mhsanaei/3x-ui/v2/web/global"
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/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())
|
||||||
|
|
||||||
@@ -32,7 +35,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.Warn:
|
case config.Warning:
|
||||||
logger.InitLogger(logging.WARNING)
|
logger.InitLogger(logging.WARNING)
|
||||||
case config.Error:
|
case config.Error:
|
||||||
logger.InitLogger(logging.ERROR)
|
logger.InitLogger(logging.ERROR)
|
||||||
@@ -111,6 +114,7 @@ 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 {
|
||||||
@@ -127,6 +131,7 @@ 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{}
|
||||||
@@ -176,6 +181,7 @@ 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()
|
||||||
@@ -195,6 +201,7 @@ func updateTgbotEnableSts(status bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateTgbotSetting updates Telegram bot settings including token, chat ID, and runtime schedule.
|
||||||
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
||||||
err := database.InitDB(config.GetDBPath())
|
err := database.InitDB(config.GetDBPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -232,6 +239,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateSetting updates various panel settings including port, credentials, base path, listen IP, and two-factor authentication.
|
||||||
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) {
|
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 {
|
||||||
@@ -290,6 +298,7 @@ func updateSetting(port int, username string, password string, webBasePath strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateCert updates the SSL certificate files for the panel.
|
||||||
func updateCert(publicKey string, privateKey string) {
|
func updateCert(publicKey string, privateKey string) {
|
||||||
err := database.InitDB(config.GetDBPath())
|
err := database.InitDB(config.GetDBPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -317,6 +326,7 @@ 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{}
|
||||||
@@ -334,6 +344,7 @@ 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 {
|
||||||
|
|
||||||
@@ -348,6 +359,7 @@ func GetListenIP(getListen bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// migrateDb performs database migration operations for the 3x-ui panel.
|
||||||
func migrateDb() {
|
func migrateDb() {
|
||||||
inboundService := service.InboundService{}
|
inboundService := service.InboundService{}
|
||||||
|
|
||||||
@@ -360,6 +372,8 @@ 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()
|
||||||
|
|||||||
73
sub/sub.go
73
sub/sub.go
@@ -1,3 +1,5 @@
|
|||||||
|
// 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 (
|
||||||
@@ -13,13 +15,13 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
webpkg "github.com/mhsanaei/3x-ui/web"
|
webpkg "github.com/mhsanaei/3x-ui/v2/web"
|
||||||
"github.com/mhsanaei/3x-ui/web/locale"
|
"github.com/mhsanaei/3x-ui/v2/web/locale"
|
||||||
"github.com/mhsanaei/3x-ui/web/middleware"
|
"github.com/mhsanaei/3x-ui/v2/web/middleware"
|
||||||
"github.com/mhsanaei/3x-ui/web/network"
|
"github.com/mhsanaei/3x-ui/v2/web/network"
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -39,6 +41,7 @@ func setEmbeddedTemplates(engine *gin.Engine) error {
|
|||||||
return nil
|
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
|
||||||
@@ -50,6 +53,7 @@ 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{
|
||||||
@@ -58,6 +62,8 @@ 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
|
// Always run in release mode for the subscription server
|
||||||
gin.DefaultWriter = io.Discard
|
gin.DefaultWriter = io.Discard
|
||||||
@@ -92,8 +98,14 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set base_path based on LinksPath for template rendering
|
// 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) {
|
engine.Use(func(c *gin.Context) {
|
||||||
c.Set("base_path", LinksPath)
|
c.Set("base_path", basePath)
|
||||||
})
|
})
|
||||||
|
|
||||||
Encrypt, err := s.settingService.GetSubEncrypt()
|
Encrypt, err := s.settingService.GetSubEncrypt()
|
||||||
@@ -173,22 +185,48 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets"
|
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 {
|
if _, err := os.Stat("web/assets"); err == nil {
|
||||||
engine.StaticFS("/assets", http.FS(os.DirFS("web/assets")))
|
assetsFS = http.FS(os.DirFS("web/assets"))
|
||||||
if linksPathForAssets != "/assets" {
|
|
||||||
engine.StaticFS(linksPathForAssets, http.FS(os.DirFS("web/assets")))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
|
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
|
||||||
engine.StaticFS("/assets", http.FS(subFS))
|
assetsFS = http.FS(subFS)
|
||||||
if linksPathForAssets != "/assets" {
|
|
||||||
engine.StaticFS(linksPathForAssets, http.FS(subFS))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logger.Error("sub: failed to mount embedded assets:", err)
|
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(
|
||||||
@@ -222,6 +260,7 @@ func (s *Server) getHtmlFiles() ([]string, error) {
|
|||||||
return files, nil
|
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() {
|
||||||
@@ -295,6 +334,7 @@ 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()
|
||||||
|
|
||||||
@@ -309,6 +349,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/config"
|
"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
|
||||||
@@ -22,6 +23,7 @@ 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,
|
||||||
@@ -53,6 +55,8 @@ 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)
|
||||||
gLink.GET(":subid", a.subs)
|
gLink.GET(":subid", a.subs)
|
||||||
@@ -62,6 +66,7 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c)
|
||||||
@@ -82,7 +87,20 @@ func (a *SUBController) subs(c *gin.Context) {
|
|||||||
if !a.jsonEnabled {
|
if !a.jsonEnabled {
|
||||||
subJsonURL = ""
|
subJsonURL = ""
|
||||||
}
|
}
|
||||||
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, 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{
|
c.HTML(200, "subpage.html", gin.H{
|
||||||
"title": "subscription.title",
|
"title": "subscription.title",
|
||||||
"cur_ver": config.GetVersion(),
|
"cur_ver": config.GetVersion(),
|
||||||
@@ -119,6 +137,7 @@ 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)
|
_, host, _, _ := a.subService.ResolveRequest(c)
|
||||||
@@ -134,6 +153,7 @@ func (a *SUBController) subJsons(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title.
|
||||||
func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
|
func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
|
||||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||||
c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
|
c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
|
||||||
|
|||||||
@@ -6,17 +6,18 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/util/json_util"
|
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||||
"github.com/mhsanaei/3x-ui/util/random"
|
"github.com/mhsanaei/3x-ui/v2/util/random"
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
"github.com/mhsanaei/3x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/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
|
||||||
@@ -28,6 +29,7 @@ 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
|
||||||
@@ -67,6 +69,7 @@ func NewSubJsonService(fragment string, noises string, mux string, rules string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetJson generates a JSON subscription configuration for the given subscription ID and host.
|
||||||
func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
|
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 {
|
||||||
@@ -171,12 +174,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["tslSettings"] = map[string]any{}
|
newStream["tlsSettings"] = map[string]any{}
|
||||||
}
|
}
|
||||||
case "none":
|
case "none":
|
||||||
if newStream["security"] != "none" {
|
if newStream["security"] != "none" {
|
||||||
newStream["security"] = "none"
|
newStream["security"] = "none"
|
||||||
delete(newStream, "tslSettings")
|
delete(newStream, "tlsSettings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
streamSettings, _ := json.MarshalIndent(newStream, "", " ")
|
streamSettings, _ := json.MarshalIndent(newStream, "", " ")
|
||||||
@@ -185,13 +188,9 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
|||||||
|
|
||||||
switch inbound.Protocol {
|
switch inbound.Protocol {
|
||||||
case "vmess":
|
case "vmess":
|
||||||
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client, ""))
|
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client))
|
||||||
case "vless":
|
case "vless":
|
||||||
var vlessSettings model.VLESSSettings
|
newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client))
|
||||||
_ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings)
|
|
||||||
|
|
||||||
newOutbounds = append(newOutbounds,
|
|
||||||
s.genVnext(inbound, streamSettings, client, vlessSettings.Encryption))
|
|
||||||
case "trojan", "shadowsocks":
|
case "trojan", "shadowsocks":
|
||||||
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
|
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
|
||||||
}
|
}
|
||||||
@@ -290,7 +289,35 @@ func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
|
|||||||
return rltyData
|
return rltyData
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, encryption string) json_util.RawMessage {
|
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||||
|
outbound := Outbound{}
|
||||||
|
usersData := make([]UserVnext, 1)
|
||||||
|
|
||||||
|
usersData[0].ID = client.ID
|
||||||
|
usersData[0].Email = client.Email
|
||||||
|
usersData[0].Security = client.Security
|
||||||
|
vnextData := make([]VnextSetting, 1)
|
||||||
|
vnextData[0] = VnextSetting{
|
||||||
|
Address: inbound.Listen,
|
||||||
|
Port: inbound.Port,
|
||||||
|
Users: usersData,
|
||||||
|
}
|
||||||
|
|
||||||
|
outbound.Protocol = string(inbound.Protocol)
|
||||||
|
outbound.Tag = "proxy"
|
||||||
|
if s.mux != "" {
|
||||||
|
outbound.Mux = json_util.RawMessage(s.mux)
|
||||||
|
}
|
||||||
|
outbound.StreamSettings = streamSettings
|
||||||
|
outbound.Settings = map[string]any{
|
||||||
|
"vnext": vnextData,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||||
outbound := Outbound{}
|
outbound := Outbound{}
|
||||||
outbound.Protocol = string(inbound.Protocol)
|
outbound.Protocol = string(inbound.Protocol)
|
||||||
outbound.Tag = "proxy"
|
outbound.Tag = "proxy"
|
||||||
@@ -298,20 +325,22 @@ 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
|
||||||
// Emit flattened settings inside Settings to match new Xray format
|
|
||||||
settings := make(map[string]any)
|
settings := make(map[string]any)
|
||||||
settings["address"] = inbound.Listen
|
settings["address"] = inbound.Listen
|
||||||
settings["port"] = inbound.Port
|
settings["port"] = inbound.Port
|
||||||
settings["id"] = client.ID
|
settings["id"] = client.ID
|
||||||
if inbound.Protocol == model.VLESS {
|
if client.Flow != "" {
|
||||||
settings["flow"] = 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
|
settings["encryption"] = encryption
|
||||||
}
|
}
|
||||||
if inbound.Protocol == model.VMESS {
|
|
||||||
settings["security"] = client.Security
|
|
||||||
}
|
|
||||||
outbound.Settings = settings
|
|
||||||
|
|
||||||
|
outbound.Settings = settings
|
||||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -363,7 +392,17 @@ type Outbound struct {
|
|||||||
Settings map[string]any `json:"settings,omitempty"`
|
Settings map[string]any `json:"settings,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy vnext-related structs removed for flattened schema
|
type VnextSetting struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Users []UserVnext `json:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserVnext struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
Security string `json:"security,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type ServerSetting struct {
|
type ServerSetting struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
|||||||
@@ -11,15 +11,16 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"github.com/mhsanaei/3x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"github.com/mhsanaei/3x-ui/util/random"
|
"github.com/mhsanaei/3x-ui/v2/util/random"
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
"github.com/mhsanaei/3x-ui/xray"
|
"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
|
||||||
@@ -29,6 +30,7 @@ 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,
|
||||||
@@ -36,6 +38,7 @@ 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, int64, xray.ClientTraffic, 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
|
||||||
@@ -318,9 +321,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||||||
if inbound.Protocol != model.VLESS {
|
if inbound.Protocol != model.VLESS {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
var vlessSettings model.VLESSSettings
|
|
||||||
_ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings)
|
|
||||||
|
|
||||||
var stream map[string]any
|
var stream map[string]any
|
||||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||||
clients, _ := s.inboundService.GetClients(inbound)
|
clients, _ := s.inboundService.GetClients(inbound)
|
||||||
@@ -335,11 +335,15 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||||||
port := inbound.Port
|
port := inbound.Port
|
||||||
streamNetwork := stream["network"].(string)
|
streamNetwork := stream["network"].(string)
|
||||||
params := make(map[string]string)
|
params := make(map[string]string)
|
||||||
if vlessSettings.Encryption != "" {
|
|
||||||
params["encryption"] = vlessSettings.Encryption
|
|
||||||
}
|
|
||||||
params["type"] = streamNetwork
|
params["type"] = streamNetwork
|
||||||
|
|
||||||
|
// 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)
|
||||||
@@ -1008,6 +1012,7 @@ func searchHost(headers any) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PageData is a view model for subpage.html
|
// PageData is a view model for subpage.html
|
||||||
|
// PageData contains data for rendering the subscription information page.
|
||||||
type PageData struct {
|
type PageData struct {
|
||||||
Host string
|
Host string
|
||||||
BasePath string
|
BasePath string
|
||||||
@@ -1029,6 +1034,7 @@ type PageData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ResolveRequest extracts scheme and host info from request/headers consistently.
|
// 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) {
|
func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) {
|
||||||
// scheme
|
// scheme
|
||||||
scheme = "http"
|
scheme = "http"
|
||||||
@@ -1071,23 +1077,78 @@ func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildURLs constructs absolute subscription and json URLs.
|
// 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) {
|
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) {
|
||||||
if strings.HasSuffix(subPath, "/") {
|
// Input validation
|
||||||
subURL = scheme + "://" + hostWithPort + subPath + subId
|
if subId == "" {
|
||||||
} else {
|
return "", ""
|
||||||
subURL = scheme + "://" + hostWithPort + strings.TrimRight(subPath, "/") + "/" + subId
|
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(subJsonPath, "/") {
|
|
||||||
subJsonURL = scheme + "://" + hostWithPort + subJsonPath + subId
|
// Get configured URIs first (highest priority)
|
||||||
} else {
|
configuredSubURI, _ := s.settingService.GetSubURI()
|
||||||
subJsonURL = scheme + "://" + hostWithPort + strings.TrimRight(subJsonPath, "/") + "/" + subId
|
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)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
|
// 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 parses header and prepares the template view model.
|
||||||
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string) PageData {
|
// 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)
|
download := common.FormatTraffic(traffic.Down)
|
||||||
upload := common.FormatTraffic(traffic.Up)
|
upload := common.FormatTraffic(traffic.Up)
|
||||||
total := "∞"
|
total := "∞"
|
||||||
@@ -1095,10 +1156,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
|
|||||||
remained := ""
|
remained := ""
|
||||||
if traffic.Total > 0 {
|
if traffic.Total > 0 {
|
||||||
total = common.FormatTraffic(traffic.Total)
|
total = common.FormatTraffic(traffic.Total)
|
||||||
left := traffic.Total - (traffic.Up + traffic.Down)
|
left := max(traffic.Total-(traffic.Up+traffic.Down), 0)
|
||||||
if left < 0 {
|
|
||||||
left = 0
|
|
||||||
}
|
|
||||||
remained = common.FormatTraffic(left)
|
remained = common.FormatTraffic(left)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1109,7 +1167,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
|
|||||||
|
|
||||||
return PageData{
|
return PageData{
|
||||||
Host: hostHeader,
|
Host: hostHeader,
|
||||||
BasePath: "/", // kept as "/"; templates now use context base_path injected from router
|
BasePath: basePath,
|
||||||
SId: subId,
|
SId: subId,
|
||||||
Download: download,
|
Download: download,
|
||||||
Upload: upload,
|
Upload: upload,
|
||||||
|
|||||||
258
update.sh
Executable file
258
update.sh
Executable file
@@ -0,0 +1,258 @@
|
|||||||
|
#!/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
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
|
// 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/logger"
|
"github.com/mhsanaei/3x-ui/v2/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 {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ 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: ")
|
||||||
@@ -16,6 +18,7 @@ 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 {
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
|
// 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
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
|
// 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: Customize json.RawMessage default behavior
|
// MarshalJSON customizes the JSON marshaling behavior for RawMessage.
|
||||||
|
// Empty RawMessage values are marshaled as "null" instead of "[]".
|
||||||
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||||
if len(m) == 0 {
|
if len(m) == 0 {
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
@@ -14,7 +17,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON: sets *m to a copy of data.
|
// UnmarshalJSON sets *m to a copy of the JSON data.
|
||||||
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
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")
|
||||||
|
|||||||
144
util/ldap/ldap.go
Normal file
144
util/ldap/ldap.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package ldaputil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
UseTLS bool
|
||||||
|
BindDN string
|
||||||
|
Password string
|
||||||
|
BaseDN string
|
||||||
|
UserFilter string
|
||||||
|
UserAttr string
|
||||||
|
FlagField string
|
||||||
|
TruthyVals []string
|
||||||
|
Invert bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchVlessFlags returns map[email]enabled
|
||||||
|
func FetchVlessFlags(cfg Config) (map[string]bool, error) {
|
||||||
|
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||||
|
var conn *ldap.Conn
|
||||||
|
var err error
|
||||||
|
if cfg.UseTLS {
|
||||||
|
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
|
||||||
|
} else {
|
||||||
|
conn, err = ldap.Dial("tcp", addr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if cfg.BindDN != "" {
|
||||||
|
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.UserFilter == "" {
|
||||||
|
cfg.UserFilter = "(objectClass=person)"
|
||||||
|
}
|
||||||
|
if cfg.UserAttr == "" {
|
||||||
|
cfg.UserAttr = "mail"
|
||||||
|
}
|
||||||
|
// if field not set we fallback to legacy vless_enabled
|
||||||
|
if cfg.FlagField == "" {
|
||||||
|
cfg.FlagField = "vless_enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
req := ldap.NewSearchRequest(
|
||||||
|
cfg.BaseDN,
|
||||||
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||||
|
cfg.UserFilter,
|
||||||
|
[]string{cfg.UserAttr, cfg.FlagField},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
res, err := conn.Search(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]bool, len(res.Entries))
|
||||||
|
for _, e := range res.Entries {
|
||||||
|
user := e.GetAttributeValue(cfg.UserAttr)
|
||||||
|
if user == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val := e.GetAttributeValue(cfg.FlagField)
|
||||||
|
enabled := false
|
||||||
|
for _, t := range cfg.TruthyVals {
|
||||||
|
if val == t {
|
||||||
|
enabled = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg.Invert {
|
||||||
|
enabled = !enabled
|
||||||
|
}
|
||||||
|
result[user] = enabled
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password.
|
||||||
|
func AuthenticateUser(cfg Config, username, password string) (bool, error) {
|
||||||
|
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||||
|
var conn *ldap.Conn
|
||||||
|
var err error
|
||||||
|
if cfg.UseTLS {
|
||||||
|
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
|
||||||
|
} else {
|
||||||
|
conn, err = ldap.Dial("tcp", addr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Optional initial bind for search
|
||||||
|
if cfg.BindDN != "" {
|
||||||
|
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.UserFilter == "" {
|
||||||
|
cfg.UserFilter = "(objectClass=person)"
|
||||||
|
}
|
||||||
|
if cfg.UserAttr == "" {
|
||||||
|
cfg.UserAttr = "uid"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build filter to find specific user
|
||||||
|
filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UserAttr, ldap.EscapeFilter(username))
|
||||||
|
req := ldap.NewSearchRequest(
|
||||||
|
cfg.BaseDN,
|
||||||
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false,
|
||||||
|
filter,
|
||||||
|
[]string{"dn"},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
res, err := conn.Search(req)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if len(res.Entries) == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
userDN := res.Entries[0].DN
|
||||||
|
// Try to bind as the user
|
||||||
|
if err := conn.Bind(userDN, password); err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
// Package random provides utilities for generating random strings and numbers.
|
||||||
package random
|
package random
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
"crypto/rand"
|
||||||
|
"math/big"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -13,6 +15,8 @@ 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)
|
||||||
@@ -33,14 +37,25 @@ 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++ {
|
||||||
runes[i] = allSeq[rand.Intn(len(allSeq))]
|
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(allSeq))))
|
||||||
|
if err != nil {
|
||||||
|
panic("crypto/rand failed: " + err.Error())
|
||||||
|
}
|
||||||
|
runes[i] = allSeq[idx.Int64()]
|
||||||
}
|
}
|
||||||
return string(runes)
|
return string(runes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Num generates a random integer between 0 and n-1.
|
||||||
func Num(n int) int {
|
func Num(n int) int {
|
||||||
return rand.Intn(n)
|
bn := big.NewInt(int64(n))
|
||||||
|
r, err := rand.Int(rand.Reader, bn)
|
||||||
|
if err != nil {
|
||||||
|
panic("crypto/rand failed: " + err.Error())
|
||||||
|
}
|
||||||
|
return int(r.Int64())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
// Package reflect_util provides reflection utilities for working with struct fields and values.
|
||||||
package reflect_util
|
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)
|
||||||
@@ -11,6 +13,7 @@ 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)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Package sys provides system utilities for monitoring network connections and CPU usage.
|
||||||
|
// Platform-specific implementations are provided for Windows, Linux, and macOS.
|
||||||
package sys
|
package sys
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ 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()
|
||||||
|
|
||||||
@@ -75,6 +77,8 @@ 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
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"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")
|
||||||
@@ -24,10 +25,12 @@ 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")
|
||||||
}
|
}
|
||||||
@@ -50,6 +53,8 @@ type filetime struct {
|
|||||||
HighDateTime 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 {
|
func ftToUint64(ft filetime) uint64 {
|
||||||
return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime)
|
return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime)
|
||||||
}
|
}
|
||||||
|
|||||||
1
web/assets/ant-design-vue/antd.min.js.map
Normal file
1
web/assets/ant-design-vue/antd.min.js.map
Normal file
File diff suppressed because one or more lines are too long
4
web/assets/axios/axios.min.js
vendored
4
web/assets/axios/axios.min.js
vendored
File diff suppressed because one or more lines are too long
1
web/assets/axios/axios.min.js.map
Normal file
1
web/assets/axios/axios.min.js.map
Normal file
File diff suppressed because one or more lines are too long
2
web/assets/css/custom.min.css
vendored
2
web/assets/css/custom.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -686,14 +686,7 @@ class Outbound extends CommonClass {
|
|||||||
if (this.stream?.sockopt)
|
if (this.stream?.sockopt)
|
||||||
stream = { sockopt: this.stream.sockopt.toJson() };
|
stream = { sockopt: this.stream.sockopt.toJson() };
|
||||||
}
|
}
|
||||||
// For VMess/VLESS, emit settings as a flat object
|
|
||||||
let settingsOut = this.settings instanceof CommonClass ? this.settings.toJson() : this.settings;
|
let settingsOut = this.settings instanceof CommonClass ? this.settings.toJson() : this.settings;
|
||||||
// Remove undefined/null keys
|
|
||||||
if (settingsOut && typeof settingsOut === 'object') {
|
|
||||||
Object.keys(settingsOut).forEach(k => {
|
|
||||||
if (settingsOut[k] === undefined || settingsOut[k] === null) delete settingsOut[k];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
protocol: this.protocol,
|
protocol: this.protocol,
|
||||||
settings: settingsOut,
|
settings: settingsOut,
|
||||||
@@ -1031,21 +1024,28 @@ Outbound.VmessSettings = class extends CommonClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json = {}) {
|
static fromJson(json = {}) {
|
||||||
if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VmessSettings();
|
if (!ObjectUtil.isArrEmpty(json.vnext)) {
|
||||||
return new Outbound.VmessSettings(
|
const v = json.vnext[0] || {};
|
||||||
json.address,
|
const u = ObjectUtil.isArrEmpty(v.users) ? {} : v.users[0];
|
||||||
json.port,
|
return new Outbound.VmessSettings(
|
||||||
json.id,
|
v.address,
|
||||||
json.security,
|
v.port,
|
||||||
);
|
u.id,
|
||||||
|
u.security,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toJson() {
|
toJson() {
|
||||||
return {
|
return {
|
||||||
address: this.address,
|
vnext: [{
|
||||||
port: this.port,
|
address: this.address,
|
||||||
id: this.id,
|
port: this.port,
|
||||||
security: this.security,
|
users: [{
|
||||||
|
id: this.id,
|
||||||
|
security: this.security
|
||||||
|
}]
|
||||||
|
}]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class AllSetting {
|
|||||||
this.webKeyFile = "";
|
this.webKeyFile = "";
|
||||||
this.webBasePath = "/";
|
this.webBasePath = "/";
|
||||||
this.sessionMaxAge = 360;
|
this.sessionMaxAge = 360;
|
||||||
this.pageSize = 50;
|
this.pageSize = 25;
|
||||||
this.expireDiff = 0;
|
this.expireDiff = 0;
|
||||||
this.trafficDiff = 0;
|
this.trafficDiff = 0;
|
||||||
this.remarkModel = "-ieo";
|
this.remarkModel = "-ieo";
|
||||||
@@ -50,6 +50,28 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,10 @@
|
|||||||
},
|
},
|
||||||
npvtunUrl() {
|
npvtunUrl() {
|
||||||
return this.app.subUrl;
|
return this.app.subUrl;
|
||||||
}
|
},
|
||||||
|
happUrl() {
|
||||||
|
return `happ://add/${encodeURIComponent(this.app.subUrl)}`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
renderLink,
|
renderLink,
|
||||||
|
|||||||
@@ -316,23 +316,13 @@ class ObjectUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static equals(a, b) {
|
static equals(a, b) {
|
||||||
for (const key in a) {
|
// shallow, symmetric comparison so newly added fields also affect equality
|
||||||
if (!a.hasOwnProperty(key)) {
|
const aKeys = Object.keys(a);
|
||||||
continue;
|
const bKeys = Object.keys(b);
|
||||||
}
|
if (aKeys.length !== bKeys.length) return false;
|
||||||
if (!b.hasOwnProperty(key)) {
|
for (const key of aKeys) {
|
||||||
return false;
|
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
||||||
} else if (a[key] !== b[key]) {
|
if (a[key] !== b[key]) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const key in b) {
|
|
||||||
if (!b.hasOwnProperty(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!a.hasOwnProperty(key)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
1
web/assets/moment/moment.min.js.map
Normal file
1
web/assets/moment/moment.min.js.map
Normal file
File diff suppressed because one or more lines are too long
34
web/assets/otpauth/otpauth.umd.min.js
vendored
34
web/assets/otpauth/otpauth.umd.min.js
vendored
@@ -1,19 +1,19 @@
|
|||||||
//! otpauth 9.4.0 | (c) Héctor Molinero Fernández | MIT | https://github.com/hectorm/otpauth
|
//! otpauth 9.4.1 | (c) Héctor Molinero Fernández | MIT | https://github.com/hectorm/otpauth
|
||||||
//! noble-hashes 1.7.1 | (c) Paul Miller | MIT | https://github.com/paulmillr/noble-hashes
|
//! noble-hashes 1.8.0 | (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){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
|
!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),{}))
|
||||||
;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()
|
;const{oHash:e,iHash:s,finished:i,destroyed:r,blockLen:n,outputLen:o}=this;return t.finished=i,t.destroyed=r,t.blockLen=n,t.outputLen=o,t.oHash=e._cloneInto(t.oHash),t.iHash=s._cloneInto(t.iHash),t}clone(){return this._cloneInto()}destroy(){this.destroyed=!0,this.oHash.destroy(),this.iHash.destroy()}constructor(t,s){super(),this.finished=!1,this.destroyed=!1,function(t){if("function"!=typeof t||"function"!=typeof t.create)throw new Error("Hash should be wrapped by utils.createHasher");e(t.outputLen),e(t.blockLen)}(t);const i=u(s);if(this.iHash=t.create(),"function"!=typeof this.iHash.update)throw new Error("Expected instance of class which extends utils.Hash");this.blockLen=this.iHash.blockLen,this.outputLen=this.iHash.outputLen;const r=this.blockLen,o=new Uint8Array(r);o.set(i.length>r?t.create().update(i).digest():i);for(let t=0;t<o.length;t++)o[t]^=54;this.iHash.update(o),this.oHash=t.create();for(let t=0;t<o.length;t++)o[t]^=106;this.oHash.update(o),n(o)}}const g=(t,e,s)=>new b(t,e).update(s).digest();function p(t,e,s){return t&e^~t&s}function w(t,e,s){return t&e^t&s^e&s}g.create=(t,e)=>new b(t,e);class y extends f{update(t){i(this),s(t=u(t));const{view:e,buffer:r,blockLen:n}=this,h=t.length;for(let s=0;s<h;){const i=Math.min(n-this.pos,h-s);if(i===n){const e=o(t);for(;n<=h-s;s+=n)this.process(e,s);continue}r.set(t.subarray(s,s+i),this.pos),this.pos+=i,s+=i,this.pos===n&&(this.process(e,0),this.pos=0)}return this.length+=t.length,this.roundClean(),this}digestInto(t){i(this),r(t,this),this.finished=!0;const{buffer:e,view:s,blockLen:h,isLE:a}=this;let{pos:c}=this;e[c++]=128,n(this.buffer.subarray(c)),this.padOffset>h-c&&(this.process(s,0),c=0);for(let t=c;t<h;t++)e[t]=0;!function(t,e,s,i){if("function"==typeof t.setBigUint64)return t.setBigUint64(e,s,i);const r=BigInt(32),n=BigInt(4294967295),o=Number(s>>r&n),h=Number(s&n),a=i?4:0,c=i?0:4;t.setUint32(e+a,o,i),t.setUint32(e+c,h,i)}(s,h-8,BigInt(8*this.length),a),this.process(s,0);const l=o(t),u=this.outputLen
|
||||||
;if(u>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]}}
|
;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,
|
||||||
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}}
|
this.set(s,i,r,n,o)}roundClean(){n(L)}destroy(){this.set(0,0,0,0,0),n(this.buffer)}constructor(){super(64,20,8,!1),this.A=0|I[0],this.B=0|I[1],this.C=0|I[2],this.D=0|I[3],this.E=0|I[4]}}const U=d(()=>new E),B=BigInt(2**32-1),S=BigInt(32);function O(t,e=!1){return e?{h:Number(t&B),l:Number(t>>S&B)}:{h:0|Number(t>>S&B),l:0|Number(t&B)}}function C(t,e=!1){const s=t.length;let i=new Uint32Array(s),r=new Uint32Array(s);for(let n=0;n<s;n++){const{h:s,l:o}=O(t[n],e);[i[n],r[n]]=[s,o]}return[i,r]}const v=(t,e,s)=>t>>>s,k=(t,e,s)=>t<<32-s|e>>>s,$=(t,e,s)=>t>>>s|e<<32-s,T=(t,e,s)=>t<<32-s|e>>>s,D=(t,e,s)=>t<<64-s|e>>>s-32,_=(t,e,s)=>t>>>s-32|e<<64-s;function F(t,e,s,i){const r=(e>>>0)+(i>>>0);return{h:t+s+(r/2**32|0)|0,l:0|r}}const G=(t,e,s)=>(t>>>0)+(e>>>0)+(s>>>0),P=(t,e,s,i)=>e+s+i+(t/2**32|0)|0,j=(t,e,s,i)=>(t>>>0)+(e>>>0)+(s>>>0)+(i>>>0),M=(t,e,s,i,r)=>e+s+i+r+(t/2**32|0)|0,R=(t,e,s,i,r)=>(t>>>0)+(e>>>0)+(s>>>0)+(i>>>0)+(r>>>0),N=(t,e,s,i,r,n)=>e+s+i+r+n+(t/2**32|0)|0,X=Uint32Array.from([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),V=new Uint32Array(64);class Z extends y{get(){const{A:t,B:e,C:s,D:i,E:r,F:n,G:o,H:h}=this;return[t,e,s,i,r,n,o,h]}set(t,e,s,i,r,n,o,h){this.A=0|t,this.B=0|e,this.C=0|s,this.D=0|i,this.E=0|r,this.F=0|n,this.G=0|o,this.H=0|h}process(t,e){for(let s=0;s<16;s++,e+=4)V[s]=t.getUint32(e,!1);for(let t=16;t<64;t++){
|
||||||
const E=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 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]}}
|
||||||
},[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,
|
const J=(()=>C(["0x428a2f98d728ae22","0x7137449123ef65cd","0xb5c0fbcfec4d3b2f","0xe9b5dba58189dbbc","0x3956c25bf348b538","0x59f111f1b605d019","0x923f82a4af194f9b","0xab1c5ed5da6d8118","0xd807aa98a3030242","0x12835b0145706fbe","0x243185be4ee4b28c","0x550c7dc3d5ffb4e2","0x72be5d74f27b896f","0x80deb1fe3b1696b1","0x9bdc06a725c71235","0xc19bf174cf692694","0xe49b69c19ef14ad2","0xefbe4786384f25e3","0x0fc19dc68b8cd5b5","0x240ca1cc77ac9c65","0x2de92c6f592b0275","0x4a7484aa6ea6e483","0x5cb0a9dcbd41fbd4","0x76f988da831153b5","0x983e5152ee66dfab","0xa831c66d2db43210","0xb00327c898fb213f","0xbf597fc7beef0ee4","0xc6e00bf33da88fc2","0xd5a79147930aa725","0x06ca6351e003826f","0x142929670a0e6e70","0x27b70a8546d22ffc","0x2e1b21385c26c926","0x4d2c6dfc5ac42aed","0x53380d139d95b3df","0x650a73548baf63de","0x766a0abb3c77b2a8","0x81c2c92e47edaee6","0x92722c851482353b","0xa2bfe8a14cf10364","0xa81a664bbc423001","0xc24b8b70d0f89791","0xc76c51a30654be30","0xd192e819d6ef5218","0xd69906245565a910","0xf40e35855771202a","0x106aa07032bbd1b8","0x19a4c116b8d2d0c8","0x1e376c085141ab53","0x2748774cdf8eeb99","0x34b0bcb5e19b48a8","0x391c0cb3c5c95a63","0x4ed8aa4ae3418acb","0x5b9cca4f7763e373","0x682e6ff3d6b2b8a3","0x748f82ee5defb2fc","0x78a5636f43172f60","0x84c87814a1f0ab72","0x8cc702081a6439ec","0x90befffa23631e28","0xa4506cebde82bde9","0xbef9a3f7b2c67915","0xc67178f2e372532b","0xca273eceea26619c","0xd186b8c721c0c207","0xeada7dd6cde0eb1e","0xf57d4f7fee6ed178","0x06f067aa72176fba","0x0a637dc5a2c898a6","0x113f9804bef90dae","0x1b710b35131c471b","0x28db77f523047d84","0x32caab7b40c72493","0x3c9ebe0a15c9bebc","0x431d67c49c100d4c","0x4cc5d4becb3e42b6","0x597f299cfc657e2a","0x5fcb6fab3ad6faec","0x6c44198c4a475817"].map(t=>BigInt(t))))(),K=(()=>J[0])(),Q=(()=>J[1])(),W=new Uint32Array(80),Y=new Uint32Array(80);class q extends y{get(){const{Ah:t,Al:e,Bh:s,Bl:i,Ch:r,Cl:n,Dh:o,Dl:h,Eh:a,El:c,Fh:l,Fl:u,Gh:f,Gl:d,Hh:b,Hl:g}=this;return[t,e,s,i,r,n,o,h,a,c,l,u,f,d,b,g]}set(t,e,s,i,r,n,o,h,a,c,l,u,f,d,b,g){this.Ah=0|t,this.Al=0|e,this.Bh=0|s,this.Bl=0|i,this.Ch=0|r,
|
||||||
this.Dl=0|h,this.Eh=0|a,this.El=0|l,this.Fh=0|c,this.Fl=0|u,this.Gh=0|d,this.Gl=0|f,this.Hh=0|b,this.Hl=0|g}process(t,e){for(let s=0;s<16;s++,e+=4)j[s]=t.getUint32(e),M[s]=t.getUint32(e+=4);for(let t=16;t<80;t++){const e=0|j[t-15],s=0|M[t-15],i=F.rotrSH(e,s,1)^F.rotrSH(e,s,8)^F.shrSH(e,s,7),r=F.rotrSL(e,s,1)^F.rotrSL(e,s,8)^F.shrSL(e,s,7),n=0|j[t-2],o=0|M[t-2],h=F.rotrSH(n,o,19)^F.rotrBH(n,o,61)^F.shrSH(n,o,6),a=F.rotrSL(n,o,19)^F.rotrBL(n,o,61)^F.shrSL(n,o,6),l=F.add4L(r,a,M[t-7],M[t-16]),c=F.add4H(l,i,h,j[t-7],j[t-16]);j[t]=0|c,M[t]=0|l}let{Ah:s,Al:i,Bh:r,Bl:n,Ch:o,Cl:h,Dh:a,Dl:l,Eh:c,El:u,Fh:d,Fl:f,Gh:b,Gl:g,Hh:p,Hl:w}=this;for(let t=0;t<80;t++){const e=F.rotrSH(c,u,14)^F.rotrSH(c,u,18)^F.rotrBH(c,u,41),y=F.rotrSL(c,u,14)^F.rotrSL(c,u,18)^F.rotrBL(c,u,41),x=c&d^~c&b,A=u&f^~u&g,m=F.add5L(w,y,A,P[t],M[t]),H=F.add5H(m,p,e,x,G[t],j[t]),L=0|m,I=F.rotrSH(s,i,28)^F.rotrBH(s,i,34)^F.rotrBH(s,i,39),S=F.rotrSL(s,i,28)^F.rotrBL(s,i,34)^F.rotrBL(s,i,39),B=s&r^s&o^r&o,E=i&n^i&h^n&h;p=0|b,w=0|g,b=0|d,g=0|f,d=0|c,f=0|u,({h:c,l:u}=F.add(0|a,0|l,0|H,0|L)),a=0|o,l=0|h,o=0|r,h=0|n,r=0|s,n=0|i;const U=F.add3L(L,S,E);s=F.add3H(U,H,I,B),i=0|U}({h:s,l:i}=F.add(0|this.Ah,0|this.Al,0|s,0|i)),({h:r,l:n}=F.add(0|this.Bh,0|this.Bl,0|r,0|n)),({h:o,l:h}=F.add(0|this.Ch,0|this.Cl,0|o,0|h)),({h:a,l}=F.add(0|this.Dh,0|this.Dl,0|a,0|l)),({h:c,l:u}=F.add(0|this.Eh,0|this.El,0|c,0|u)),({h:d,l:f}=F.add(0|this.Fh,0|this.Fl,0|d,0|f)),({h:b,l:g}=F.add(0|this.Gh,0|this.Gl,0|b,0|g)),({h:p,l:w}=F.add(0|this.Hh,0|this.Hl,0|p,0|w)),this.set(s,i,r,n,o,h,a,l,c,u,d,f,b,g,p,w)}roundClean(){j.fill(0),M.fill(0)}destroy(){this.buffer.fill(0),this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)}constructor(){super(128,64,16,!1),this.Ah=1779033703,this.Al=-205731576,this.Bh=-1150833019,this.Bl=-2067093701,this.Ch=1013904242,this.Cl=-23791573,this.Dh=-1521486534,this.Dl=1595750129,this.Eh=1359893119,this.El=-1377402159,this.Fh=-1694144372,this.Fl=725511199,this.Gh=528734635,this.Gl=-79577749,this.Hh=1541459225,this.Hl=327033209}}class N extends R{constructor(){super(),
|
this.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.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){
|
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))}
|
||||||
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):
|
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}`)}
|
||||||
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)}),
|
},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,
|
||||||
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
|
value:kt(this.bytes)}),this.hex}constructor({buffer:t,size:e=20}={}){this.bytes=void 0===t?(t=>{if(Et.crypto?.getRandomValues)return Et.crypto.getRandomValues(new Uint8Array(t));throw new Error("Cryptography API not available")})(e):new Uint8Array(t),Object.defineProperty(this,"bytes",{enumerable:!0,writable:!1,configurable:!1,value:this.bytes})}}class jt{static get defaults(){return{issuer:"",label:"OTPAuth",issuerInLabel:!0,algorithm:"SHA1",digits:6,counter:0,window:1}}static generate({secret:t,algorithm:e=jt.defaults.algorithm,digits:s=jt.defaults.digits,counter:i=jt.defaults.counter}){const r=((t,e,s)=>{if(g){const i=Ut[t]??Ut[Bt(t)];return g(i,e,s)}throw new Error("Missing HMAC function")})(e,t.bytes,(t=>{const e=new ArrayBuffer(8),s=new Uint8Array(e);let i=t;for(let t=7;t>=0&&0!==i;t--)s[t]=255&i,i-=s[t],i/=256;return s})(i)),n=15&r[r.byteLength-1];return(((127&r[n])<<24|(255&r[n+1])<<16|(255&r[n+2])<<8|255&r[n+3])%10**s).toString().padStart(s,"0")}generate({counter:t=this.counter++}={}){return jt.generate({secret:this.secret,algorithm:this.algorithm,digits:this.digits,counter:t})}static validate({token:t,secret:e,algorithm:s,digits:i=jt.defaults.digits,counter:r=jt.defaults.counter,window:n=jt.defaults.window}){if(t.length!==i)return null;let o=null;const h=n=>{const h=jt.generate({secret:e,algorithm:s,digits:i,counter:n});((t,e)=>{{if(t.length!==e.length)throw new TypeError("Input strings must have the same length");let s=-1,i=0;for(;++s<t.length;)i|=t.charCodeAt(s)^e.charCodeAt(s);return 0===i}})(t,h)&&(o=n-r)};h(r);for(let t=1;t<=n&&null===o&&(h(r-t),null===o)&&(h(r+t),null===o);++t);return o}validate({token:t,counter:e=this.counter,window:s}){return jt.validate({token:t,secret:this.secret,algorithm:this.algorithm,digits:this.digits,counter:e,window:s})}toString(){const t=encodeURIComponent
|
||||||
;return"otpauth://hotp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`counter=${t(this.counter)}`}constructor({issuer:t=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://hotp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`counter=${t(this.counter)}`}constructor({issuer:t=jt.defaults.issuer,label:e=jt.defaults.label,issuerInLabel:s=jt.defaults.issuerInLabel,secret:i=new Pt,algorithm:r=jt.defaults.algorithm,digits:n=jt.defaults.digits,counter:o=jt.defaults.counter}={}){this.issuer=t,this.label=e,this.issuerInLabel=s,this.secret="string"==typeof i?Pt.fromBase32(i):i,this.algorithm=Bt(r),this.digits=n,this.counter=o}}class Mt{static get defaults(){return{issuer:"",label:"OTPAuth",issuerInLabel:!0,algorithm:"SHA1",digits:6,period:30,window:1}}static counter({period:t=Mt.defaults.period,timestamp:e=Date.now()}={}){return Math.floor(e/1e3/t)}counter({timestamp:t=Date.now()}={}){return Mt.counter({period:this.period,timestamp:t})}static remaining({period:t=Mt.defaults.period,timestamp:e=Date.now()}={}){return 1e3*t-e%(1e3*t)}remaining({timestamp:t=Date.now()}={}){return Mt.remaining({period:this.period,timestamp:t})}static generate({secret:t,algorithm:e,digits:s,period:i=Mt.defaults.period,timestamp:r=Date.now()}){return jt.generate({secret:t,algorithm:e,digits:s,counter:Mt.counter({period:i,timestamp:r})})}generate({timestamp:t=Date.now()}={}){return Mt.generate({secret:this.secret,algorithm:this.algorithm,digits:this.digits,period:this.period,timestamp:t})}static validate({token:t,secret:e,algorithm:s,digits:i,period:r=Mt.defaults.period,timestamp:n=Date.now(),window:o}){return jt.validate({token:t,secret:e,algorithm:s,digits:i,counter:Mt.counter({period:r,timestamp:n}),window:o})}validate({token:t,timestamp:e,window:s}){return Mt.validate({token:t,secret:this.secret,algorithm:this.algorithm,digits:this.digits,period:this.period,timestamp:e,window:s})}toString(){const t=encodeURIComponent
|
||||||
;return"otpauth://totp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`period=${t(this.period)}`}constructor({issuer:t=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){
|
;return"otpauth://totp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`period=${t(this.period)}`}constructor({issuer:t=Mt.defaults.issuer,label:e=Mt.defaults.label,issuerInLabel:s=Mt.defaults.issuerInLabel,secret:i=new Pt,algorithm:r=Mt.defaults.algorithm,digits:n=Mt.defaults.digits,period:o=Mt.defaults.period}={}){this.issuer=t,this.label=e,this.issuerInLabel=s,this.secret="string"==typeof i?Pt.fromBase32(i):i,this.algorithm=Bt(r),this.digits=n,this.period=o}}const Rt=/^otpauth:\/\/([ht]otp)\/(.+)\?([A-Z0-9.~_-]+=[^?&]*(?:&[A-Z0-9.~_-]+=[^?&]*)*)$/i,Nt=/^[2-7A-Z]+=*$/i,Xt=/^SHA(?:1|224|256|384|512|3-224|3-256|3-384|3-512)$/i,Vt=/^[+-]?\d+$/,Zt=/^\+?[1-9]\d*$/;t.HOTP=jt,t.Secret=Pt,t.TOTP=Mt,t.URI=class{static parse(t){let e;try{e=t.match(Rt)}catch(t){}if(!Array.isArray(e))throw new URIError("Invalid URI format");const s=e[1].toLowerCase(),i=e[2].split(/(?::|%3A) *(.+)/i,2).map(decodeURIComponent),r=e[3].split("&").reduce((t,e)=>{const s=e.split(/=(.*)/,2).map(decodeURIComponent),i=s[0].toLowerCase(),r=s[1],n=t;return n[i]=r,n},{});let n;const o={};if("hotp"===s){if(n=jt,void 0===r.counter||!Vt.test(r.counter))throw new TypeError("Missing or invalid 'counter' parameter");o.counter=parseInt(r.counter,10)}else{if("totp"!==s)throw new TypeError("Unknown OTP type");if(n=Mt,void 0!==r.period){if(!Zt.test(r.period))throw new TypeError("Invalid 'period' parameter");o.period=parseInt(r.period,10)}}if(void 0!==r.issuer&&(o.issuer=r.issuer),2===i.length?(o.label=i[1],void 0===o.issuer||""===o.issuer?o.issuer=i[0]:""===i[0]&&(o.issuerInLabel=!1)):(o.label=i[0],void 0!==o.issuer&&""!==o.issuer&&(o.issuerInLabel=!1)),void 0===r.secret||!Nt.test(r.secret))throw new TypeError("Missing or invalid 'secret' parameter");if(o.secret=r.secret,void 0!==r.algorithm){
|
||||||
if(!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"}));
|
if(!Xt.test(r.algorithm))throw new TypeError("Invalid 'algorithm' parameter");o.algorithm=r.algorithm}if(void 0!==r.digits){if(!Zt.test(r.digits))throw new TypeError("Invalid 'digits' parameter");o.digits=parseInt(r.digits,10)}return new n(o)}static stringify(t){if(t instanceof jt||t instanceof Mt)return t.toString();throw new TypeError("Invalid 'HOTP/TOTP' object")}},t.version="9.4.1"});
|
||||||
//# sourceMappingURL=otpauth.umd.min.js.map
|
//# sourceMappingURL=otpauth.umd.min.js.map
|
||||||
|
|||||||
1
web/assets/otpauth/otpauth.umd.min.js.map
Normal file
1
web/assets/otpauth/otpauth.umd.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -1,11 +1,15 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"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
|
||||||
@@ -13,16 +17,28 @@ type APIController struct {
|
|||||||
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
|
// Main API group
|
||||||
api := g.Group("/panel/api")
|
api := g.Group("/panel/api")
|
||||||
api.Use(a.checkLogin)
|
api.Use(a.checkAPIAuth)
|
||||||
|
|
||||||
// Inbounds API
|
// Inbounds API
|
||||||
inbounds := api.Group("/inbounds")
|
inbounds := api.Group("/inbounds")
|
||||||
@@ -36,6 +52,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
|||||||
api.GET("/backuptotgbot", a.BackuptoTgbot)
|
api.GET("/backuptotgbot", a.BackuptoTgbot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BackuptoTgbot sends a backup of the panel data to Telegram bot admins.
|
||||||
func (a *APIController) BackuptoTgbot(c *gin.Context) {
|
func (a *APIController) BackuptoTgbot(c *gin.Context) {
|
||||||
a.Tgbot.SendBackupToAdmins()
|
a.Tgbot.SendBackupToAdmins()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
|
// Package controller provides HTTP request handlers and controllers for the 3x-ui web management panel.
|
||||||
|
// It handles routing, authentication, and API endpoints for managing Xray inbounds, settings, and more.
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/web/locale"
|
"github.com/mhsanaei/3x-ui/v2/web/locale"
|
||||||
"github.com/mhsanaei/3x-ui/web/session"
|
"github.com/mhsanaei/3x-ui/v2/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) {
|
||||||
@@ -25,6 +29,7 @@ func (a *BaseController) checkLogin(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// I18nWeb retrieves an internationalized message for the web interface based on the current locale.
|
||||||
func I18nWeb(c *gin.Context, name string, params ...string) string {
|
func I18nWeb(c *gin.Context, name string, params ...string) string {
|
||||||
anyfunc, funcExists := c.Get("I18n")
|
anyfunc, funcExists := c.Get("I18n")
|
||||||
if !funcExists {
|
if !funcExists {
|
||||||
|
|||||||
@@ -5,24 +5,27 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
"github.com/mhsanaei/3x-ui/web/session"
|
"github.com/mhsanaei/3x-ui/v2/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.GET("/list", a.getInbounds)
|
g.GET("/list", a.getInbounds)
|
||||||
@@ -49,6 +52,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
|||||||
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
|
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)
|
||||||
@@ -59,6 +63,7 @@ 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 {
|
||||||
@@ -73,6 +78,7 @@ 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)
|
||||||
@@ -83,6 +89,7 @@ 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)
|
||||||
@@ -93,6 +100,7 @@ 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)
|
||||||
@@ -108,8 +116,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
needRestart := false
|
inbound, needRestart, err := a.inboundService.AddInbound(inbound)
|
||||||
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
|
||||||
@@ -120,14 +127,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 := true
|
needRestart, err := a.inboundService.DelInbound(id)
|
||||||
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
|
||||||
@@ -138,6 +145,7 @@ 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 {
|
||||||
@@ -152,8 +160,7 @@ 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
|
||||||
}
|
}
|
||||||
needRestart := true
|
inbound, needRestart, err := a.inboundService.UpdateInbound(inbound)
|
||||||
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
|
||||||
@@ -164,6 +171,7 @@ 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")
|
||||||
|
|
||||||
@@ -176,6 +184,7 @@ 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")
|
||||||
|
|
||||||
@@ -187,6 +196,7 @@ 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)
|
||||||
@@ -195,9 +205,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
needRestart := true
|
needRestart, err := a.inboundService.AddInboundClient(data)
|
||||||
|
|
||||||
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
|
||||||
@@ -208,6 +216,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delInboundClient deletes a client from an inbound by inbound ID and client ID.
|
||||||
func (a *InboundController) delInboundClient(c *gin.Context) {
|
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 {
|
||||||
@@ -216,9 +225,7 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
clientId := c.Param("clientId")
|
clientId := c.Param("clientId")
|
||||||
|
|
||||||
needRestart := true
|
needRestart, err := a.inboundService.DelInboundClient(id, clientId)
|
||||||
|
|
||||||
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
|
||||||
@@ -229,6 +236,7 @@ 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")
|
||||||
|
|
||||||
@@ -239,9 +247,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
needRestart := true
|
needRestart, err := a.inboundService.UpdateInboundClient(inbound, clientId)
|
||||||
|
|
||||||
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
|
||||||
@@ -252,6 +258,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resetClientTraffic resets the traffic counter for a specific client in an inbound.
|
||||||
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
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 {
|
||||||
@@ -271,6 +278,7 @@ 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 {
|
||||||
@@ -282,6 +290,7 @@ 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 {
|
||||||
@@ -299,6 +308,7 @@ 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)
|
||||||
@@ -328,6 +338,7 @@ func (a *InboundController) importInbound(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delDepletedClients deletes clients in an inbound who have exhausted their traffic limits.
|
||||||
func (a *InboundController) delDepletedClients(c *gin.Context) {
|
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 {
|
||||||
@@ -342,15 +353,18 @@ 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) {
|
func (a *InboundController) lastOnline(c *gin.Context) {
|
||||||
data, err := a.inboundService.GetClientsLastOnline()
|
data, err := a.inboundService.GetClientsLastOnline()
|
||||||
jsonObj(c, data, err)
|
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")
|
||||||
|
|
||||||
@@ -376,6 +390,7 @@ 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) {
|
func (a *InboundController) delInboundClientByEmail(c *gin.Context) {
|
||||||
inboundId, err := strconv.Atoi(c.Param("id"))
|
inboundId, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,20 +5,22 @@ import (
|
|||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
"github.com/mhsanaei/3x-ui/web/session"
|
"github.com/mhsanaei/3x-ui/v2/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
|
||||||
|
|
||||||
@@ -27,19 +29,23 @@ 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.POST("/login", a.login)
|
|
||||||
g.GET("/logout", a.logout)
|
g.GET("/logout", a.logout)
|
||||||
|
|
||||||
|
g.POST("/login", a.login)
|
||||||
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/")
|
||||||
@@ -48,6 +54,7 @@ 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
|
||||||
|
|
||||||
@@ -95,6 +102,7 @@ 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 {
|
||||||
@@ -107,6 +115,7 @@ 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 {
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/web/global"
|
"github.com/mhsanaei/3x-ui/v2/web/global"
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/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
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ type ServerController struct {
|
|||||||
lastGetVersionsTime int64 // unix seconds
|
lastGetVersionsTime int64 // unix seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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{}
|
||||||
a.initRouter(g)
|
a.initRouter(g)
|
||||||
@@ -34,6 +36,7 @@ func NewServerController(g *gin.RouterGroup) *ServerController {
|
|||||||
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.GET("/status", a.status)
|
g.GET("/status", a.status)
|
||||||
@@ -58,6 +61,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
|||||||
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
|
// collect cpu history when status is fresh
|
||||||
@@ -66,6 +70,7 @@ func (a *ServerController) refreshStatus() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
@@ -76,8 +81,10 @@ func (a *ServerController) startTask() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// status returns the current server status information.
|
||||||
func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) }
|
func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) }
|
||||||
|
|
||||||
|
// getCpuHistoryBucket retrieves aggregated CPU usage history based on the specified time bucket.
|
||||||
func (a *ServerController) getCpuHistoryBucket(c *gin.Context) {
|
func (a *ServerController) getCpuHistoryBucket(c *gin.Context) {
|
||||||
bucketStr := c.Param("bucket")
|
bucketStr := c.Param("bucket")
|
||||||
bucket, err := strconv.Atoi(bucketStr)
|
bucket, err := strconv.Atoi(bucketStr)
|
||||||
@@ -101,6 +108,7 @@ func (a *ServerController) getCpuHistoryBucket(c *gin.Context) {
|
|||||||
jsonObj(c, points, nil)
|
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().Unix()
|
||||||
if now-a.lastGetVersionsTime <= 60 { // 1 minute cache
|
if now-a.lastGetVersionsTime <= 60 { // 1 minute cache
|
||||||
@@ -120,18 +128,29 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
|
|||||||
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) {
|
||||||
err := a.serverService.StopXrayService()
|
err := a.serverService.StopXrayService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -141,6 +160,7 @@ 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 {
|
||||||
@@ -150,6 +170,7 @@ 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")
|
||||||
@@ -158,6 +179,7 @@ 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")
|
||||||
@@ -202,6 +224,7 @@ 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 {
|
||||||
@@ -211,6 +234,7 @@ 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 {
|
||||||
@@ -238,6 +262,7 @@ 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")
|
||||||
@@ -258,6 +283,7 @@ 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 {
|
||||||
@@ -267,6 +293,7 @@ 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 {
|
||||||
@@ -276,6 +303,7 @@ 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)
|
||||||
@@ -286,6 +314,7 @@ 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) {
|
func (a *ServerController) getNewVlessEnc(c *gin.Context) {
|
||||||
out, err := a.serverService.GetNewVlessEnc()
|
out, err := a.serverService.GetNewVlessEnc()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -295,6 +324,7 @@ func (a *ServerController) getNewVlessEnc(c *gin.Context) {
|
|||||||
jsonObj(c, out, nil)
|
jsonObj(c, out, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getNewUUID generates a new UUID.
|
||||||
func (a *ServerController) getNewUUID(c *gin.Context) {
|
func (a *ServerController) getNewUUID(c *gin.Context) {
|
||||||
uuidResp, err := a.serverService.GetNewUUID()
|
uuidResp, err := a.serverService.GetNewUUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -305,6 +335,7 @@ func (a *ServerController) getNewUUID(c *gin.Context) {
|
|||||||
jsonObj(c, uuidResp, nil)
|
jsonObj(c, uuidResp, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getNewmlkem768 generates a new ML-KEM-768 key.
|
||||||
func (a *ServerController) getNewmlkem768(c *gin.Context) {
|
func (a *ServerController) getNewmlkem768(c *gin.Context) {
|
||||||
out, err := a.serverService.GetNewmlkem768()
|
out, err := a.serverService.GetNewmlkem768()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/util/crypto"
|
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||||
"github.com/mhsanaei/3x-ui/web/entity"
|
"github.com/mhsanaei/3x-ui/v2/web/entity"
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
"github.com/mhsanaei/3x-ui/web/session"
|
"github.com/mhsanaei/3x-ui/v2/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"`
|
||||||
@@ -19,18 +20,21 @@ 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")
|
||||||
|
|
||||||
@@ -42,6 +46,7 @@ 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 {
|
||||||
@@ -51,6 +56,7 @@ 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 {
|
||||||
@@ -60,6 +66,7 @@ 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)
|
||||||
@@ -71,6 +78,7 @@ 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)
|
||||||
@@ -96,11 +104,13 @@ 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 {
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/web/entity"
|
"github.com/mhsanaei/3x-ui/v2/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 != "" {
|
||||||
@@ -27,14 +28,17 @@ 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,
|
||||||
@@ -52,6 +56,7 @@ 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,
|
||||||
@@ -59,6 +64,7 @@ func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// html renders an HTML template with the provided data and title.
|
||||||
func html(c *gin.Context, name string, title string, data gin.H) {
|
func html(c *gin.Context, name string, title string, data gin.H) {
|
||||||
if data == nil {
|
if data == nil {
|
||||||
data = gin.H{}
|
data = gin.H{}
|
||||||
@@ -81,6 +87,7 @@ func html(c *gin.Context, name string, title string, data gin.H) {
|
|||||||
c.HTML(http.StatusOK, name, getContext(data))
|
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(),
|
||||||
@@ -91,6 +98,7 @@ 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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/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
|
||||||
@@ -15,12 +16,14 @@ 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("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||||
@@ -33,6 +36,7 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
|||||||
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 {
|
||||||
@@ -48,12 +52,14 @@ 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 {
|
||||||
@@ -63,10 +69,12 @@ 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
|
||||||
@@ -90,6 +98,7 @@ 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 {
|
||||||
@@ -99,6 +108,7 @@ 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)
|
||||||
|
|||||||
@@ -4,21 +4,22 @@ 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
|
|
||||||
serverController *ServerController
|
|
||||||
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)
|
||||||
@@ -28,24 +29,26 @@ 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.serverController = NewServerController(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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package entity defines data structures and entities used by the web layer of the 3x-ui panel.
|
||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,64 +8,100 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Msg represents a standard API response message with success status, message text, and optional data object.
|
||||||
type Msg struct {
|
type Msg struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"` // Indicates if the operation was successful
|
||||||
Msg string `json:"msg"`
|
Msg string `json:"msg"` // Response message text
|
||||||
Obj any `json:"obj"`
|
Obj any `json:"obj"` // Optional data object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AllSetting contains all configuration settings for the 3x-ui panel including web server, Telegram bot, and subscription settings.
|
||||||
type AllSetting struct {
|
type AllSetting struct {
|
||||||
WebListen string `json:"webListen" form:"webListen"`
|
// Web server settings
|
||||||
WebDomain string `json:"webDomain" form:"webDomain"`
|
WebListen string `json:"webListen" form:"webListen"` // Web server listen IP address
|
||||||
WebPort int `json:"webPort" form:"webPort"`
|
WebDomain string `json:"webDomain" form:"webDomain"` // Web server domain for domain validation
|
||||||
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
WebPort int `json:"webPort" form:"webPort"` // Web server port number
|
||||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
WebCertFile string `json:"webCertFile" form:"webCertFile"` // Path to SSL certificate file for web server
|
||||||
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` // Path to SSL private key file for web server
|
||||||
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
|
WebBasePath string `json:"webBasePath" form:"webBasePath"` // Base path for web panel URLs
|
||||||
PageSize int `json:"pageSize" form:"pageSize"`
|
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"` // Session maximum age in minutes
|
||||||
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
|
|
||||||
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
|
// UI settings
|
||||||
RemarkModel string `json:"remarkModel" form:"remarkModel"`
|
PageSize int `json:"pageSize" form:"pageSize"` // Number of items per page in lists
|
||||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
ExpireDiff int `json:"expireDiff" form:"expireDiff"` // Expiration warning threshold in days
|
||||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"` // Traffic warning threshold percentage
|
||||||
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"`
|
RemarkModel string `json:"remarkModel" form:"remarkModel"` // Remark model pattern for inbounds
|
||||||
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"`
|
Datepicker string `json:"datepicker" form:"datepicker"` // Date picker format
|
||||||
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
|
|
||||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
// Telegram bot settings
|
||||||
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
|
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` // Enable Telegram bot notifications
|
||||||
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
|
TgBotToken string `json:"tgBotToken" form:"tgBotToken"` // Telegram bot token
|
||||||
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"` // Proxy URL for Telegram bot
|
||||||
TgLang string `json:"tgLang" form:"tgLang"`
|
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"` // Custom API server for Telegram bot
|
||||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"` // Telegram chat ID for notifications
|
||||||
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
TgRunTime string `json:"tgRunTime" form:"tgRunTime"` // Cron schedule for Telegram notifications
|
||||||
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` // Enable database backup via Telegram
|
||||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"` // Send login notifications
|
||||||
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"`
|
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
|
||||||
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
|
ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"` // Enable external traffic reporting
|
||||||
Datepicker string `json:"datepicker" form:"datepicker"`
|
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"`
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package global provides global variables and interfaces for accessing web and subscription servers.
|
||||||
package global
|
package global
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -12,27 +13,33 @@ var (
|
|||||||
subServer SubServer
|
subServer SubServer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WebServer interface defines methods for accessing the web server instance.
|
||||||
type WebServer interface {
|
type WebServer interface {
|
||||||
GetCron() *cron.Cron
|
GetCron() *cron.Cron // Get the cron scheduler
|
||||||
GetCtx() context.Context
|
GetCtx() context.Context // Get the server context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubServer interface defines methods for accessing the subscription server instance.
|
||||||
type SubServer interface {
|
type SubServer interface {
|
||||||
GetCtx() context.Context
|
GetCtx() context.Context // Get the server 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,18 +8,21 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// HashEntry represents a stored hash entry with its value and timestamp.
|
||||||
type HashEntry struct {
|
type HashEntry struct {
|
||||||
Hash string
|
Hash string // MD5 hash string
|
||||||
Value string
|
Value string // Original value
|
||||||
Timestamp time.Time
|
Timestamp time.Time // Time when the hash was created
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
Data map[string]HashEntry // Map of hash to entry
|
||||||
Expiration time.Duration
|
Expiration time.Duration // Expiration duration for entries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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),
|
||||||
@@ -27,6 +30,7 @@ func NewHashStorage(expiration time.Duration) *HashStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveHash generates an MD5 hash for the given query string and stores it with a timestamp.
|
||||||
func (h *HashStorage) SaveHash(query string) string {
|
func (h *HashStorage) SaveHash(query string) string {
|
||||||
h.Lock()
|
h.Lock()
|
||||||
defer h.Unlock()
|
defer h.Unlock()
|
||||||
@@ -45,6 +49,7 @@ 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()
|
||||||
@@ -54,11 +59,13 @@ 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()
|
||||||
@@ -72,6 +79,7 @@ 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()
|
||||||
|
|||||||
@@ -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: '24px' }" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
|
<a-icon :style="{ fontSize: '22px', marginInlineStart: '14px' }" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
||||||
<a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
|
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">{{ i18n "info" }}</template>
|
<template slot="title">{{ i18n "info" }}</template>
|
||||||
<a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
|
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<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: '24px', cursor: 'pointer' }" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
|
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
|
||||||
</a-popconfirm>
|
</a-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: '24px', cursor: 'pointer' }" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
|
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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="65531"></a-input-number>
|
<a-input-number v-model.number="inbound.port" :min="1" :max="65535"></a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
@@ -51,8 +51,9 @@
|
|||||||
<span>{{ i18n "pages.inbounds.periodicTrafficResetDesc" }}</span>
|
<span>{{ i18n "pages.inbounds.periodicTrafficResetDesc" }}</span>
|
||||||
<br v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
|
<br v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
|
||||||
<span v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
|
<span v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
|
||||||
<strong>{{ i18n "pages.inbounds.lastReset" }}:</strong>
|
<strong>{{ i18n "pages.inbounds.lastReset" }}:</strong>
|
||||||
<span v-if="datepicker == 'gregorian'">[[ moment(dbInbound.lastTrafficResetTime).format('YYYY-MM-DD HH:mm:ss') ]]</span>
|
<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 v-else>[[ DateUtil.convertToJalalian(moment(dbInbound.lastTrafficResetTime)) ]]</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -145,4 +146,4 @@
|
|||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
|
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -3,12 +3,14 @@
|
|||||||
<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" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"></a-button>
|
<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>
|
||||||
</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' }" :dropdown-class-name="themeSwitcher.currentTheme">
|
<a-select v-model="row.forceTls" :style="{ width: '20%', margin: '0px' }"
|
||||||
|
: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>
|
||||||
@@ -17,7 +19,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="65531"></a-input-number>
|
<a-input-number :style="{ width: '15%' }" v-model.number="row.port" min="1" max="65535"></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">
|
||||||
@@ -26,4 +28,4 @@
|
|||||||
</a-input>
|
</a-input>
|
||||||
</a-input-group>
|
</a-input-group>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -660,7 +660,7 @@
|
|||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.allTimeTraffic" }}',
|
title: '{{ i18n "pages.inbounds.allTimeTraffic" }}',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 70,
|
width: 60,
|
||||||
scopedSlots: { customRender: 'allTimeInbound' },
|
scopedSlots: { customRender: 'allTimeInbound' },
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
||||||
@@ -693,12 +693,12 @@
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
const innerColumns = [
|
const innerColumns = [
|
||||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 65, scopedSlots: { customRender: 'actions' } },
|
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 35, scopedSlots: { customRender: 'enable' } },
|
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
|
||||||
{ title: '{{ i18n "online" }}', width: 32, scopedSlots: { customRender: 'online' } },
|
{ title: '{{ i18n "online" }}', width: 32, scopedSlots: { customRender: 'online' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
|
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },
|
{ title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 60, align: 'center', scopedSlots: { customRender: 'allTime' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -736,7 +736,7 @@
|
|||||||
refreshing: false,
|
refreshing: false,
|
||||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||||
subSettings: {
|
subSettings: {
|
||||||
enable: true,
|
enable: false,
|
||||||
subTitle: '',
|
subTitle: '',
|
||||||
subURI: '',
|
subURI: '',
|
||||||
subJsonURI: '',
|
subJsonURI: '',
|
||||||
@@ -747,7 +747,7 @@
|
|||||||
tgBotEnable: false,
|
tgBotEnable: false,
|
||||||
showAlert: false,
|
showAlert: false,
|
||||||
ipLimitEnable: false,
|
ipLimitEnable: false,
|
||||||
pageSize: 50,
|
pageSize: 0,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loading(spinning = true) {
|
loading(spinning = true) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{{ 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 + ' login-app'">
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-layout-content class="under min-h-0">
|
<a-layout-content class="under min-h-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,7 +20,7 @@
|
|||||||
</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" class="h-100 overflow-y-auto overflow-x-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" class="my-3rem">
|
||||||
<template v-if="!loadingStates.fetched">
|
<template v-if="!loadingStates.fetched">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
<a-space direction="vertical" :size="10">
|
<a-space direction="vertical" :size="10">
|
||||||
<a-theme-switch-login></a-theme-switch-login>
|
<a-theme-switch-login></a-theme-switch-login>
|
||||||
<span>{{ i18n "pages.settings.language" }}</span>
|
<span>{{ i18n "pages.settings.language" }}</span>
|
||||||
<a-select ref="selectLang" class="w-100" v-model="lang"
|
<a-select ref="selectLang" class="w-100" v-model="lang" @change="LanguageManager.setLanguage(lang)"
|
||||||
@change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
||||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||||
<span v-text="l.name"></span>
|
<span v-text="l.name"></span>
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
</a-input>
|
</a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-input-password autocomplete="password" name="password" v-model.trim="user.password"
|
<a-input-password autocomplete="current-password" name="password" v-model.trim="user.password"
|
||||||
placeholder='{{ i18n "password" }}' required>
|
placeholder='{{ i18n "password" }}' required>
|
||||||
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
||||||
</a-input-password>
|
</a-input-password>
|
||||||
@@ -81,7 +81,8 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-row justify="center" class="centered">
|
<a-row justify="center" class="centered">
|
||||||
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem" :style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
|
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem"
|
||||||
|
:style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
|
||||||
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
|
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
|
||||||
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
||||||
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
||||||
@@ -107,17 +108,11 @@
|
|||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
themeSwitcher,
|
themeSwitcher,
|
||||||
loadingStates: {
|
loadingStates: { fetched: false, spinning: false },
|
||||||
fetched: false,
|
user: { username: "", password: "", twoFactorCode: "" },
|
||||||
spinning: false
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
twoFactorCode: ""
|
|
||||||
},
|
|
||||||
twoFactorEnable: false,
|
twoFactorEnable: false,
|
||||||
lang: ""
|
lang: "",
|
||||||
|
animationStarted: false
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.lang = LanguageManager.getLanguage();
|
this.lang = LanguageManager.getLanguage();
|
||||||
@@ -126,65 +121,52 @@
|
|||||||
methods: {
|
methods: {
|
||||||
async login() {
|
async login() {
|
||||||
this.loadingStates.spinning = true;
|
this.loadingStates.spinning = true;
|
||||||
|
|
||||||
const msg = await HttpUtil.post('/login', this.user);
|
const msg = await HttpUtil.post('/login', this.user);
|
||||||
|
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
location.href = basePath + 'panel/';
|
location.href = basePath + 'panel/';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadingStates.spinning = false;
|
this.loadingStates.spinning = false;
|
||||||
},
|
},
|
||||||
async getTwoFactorEnable() {
|
async getTwoFactorEnable() {
|
||||||
const msg = await HttpUtil.post('/getTwoFactorEnable');
|
const msg = await HttpUtil.post('/getTwoFactorEnable');
|
||||||
|
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
this.twoFactorEnable = msg.obj;
|
this.twoFactorEnable = msg.obj;
|
||||||
this.loadingStates.fetched = true;
|
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');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
var animationDelay = 2000;
|
|
||||||
initHeadline();
|
|
||||||
|
|
||||||
function initHeadline() {
|
|
||||||
animateHeadline(document.querySelectorAll('.headline'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function animateHeadline(headlines) {
|
|
||||||
var duration = animationDelay;
|
|
||||||
headlines.forEach(function (headline) {
|
|
||||||
setTimeout(function () {
|
|
||||||
hideWord(headline.querySelector('.is-visible'));
|
|
||||||
}, duration);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideWord(word) {
|
|
||||||
var nextWord = takeNext(word);
|
|
||||||
switchWord(word, nextWord);
|
|
||||||
setTimeout(function () {
|
|
||||||
hideWord(nextWord);
|
|
||||||
}, animationDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
function takeNext(word) {
|
|
||||||
return word.nextElementSibling ? word.nextElementSibling : word.parentElement.firstElementChild;
|
|
||||||
}
|
|
||||||
|
|
||||||
function 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';
|
const pm_input_selector = 'input.ant-input, textarea.ant-input';
|
||||||
const pm_strip_props = [
|
const pm_strip_props = [
|
||||||
'background',
|
'background',
|
||||||
@@ -260,4 +242,4 @@
|
|||||||
pm_init();
|
pm_init();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{{ template "page/body_end" .}}
|
{{ template "page/body_end" .}}
|
||||||
|
|||||||
@@ -3,22 +3,29 @@
|
|||||||
: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' }">
|
||||||
<a-row justify="space-between" align="middle">
|
<div class="ant-dns-presets-line">
|
||||||
<a-col :span="12">
|
<a-space direction="horizontal" size="small" align="center">
|
||||||
<a-space direction="vertical" size="small">
|
<a-tag :color="dns.family ? 'purple' : 'green'">[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}' : 'DNS' ]]</a-tag>
|
||||||
<span class="ant-dns-presets-list-name">[[ dns.name ]]</span>
|
<span class="ant-dns-presets-list-name">[[ dns.name ]]</span>
|
||||||
<a-tag :color="dns.family ? 'purple' : 'green'">[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}' : 'DNS' ]]</a-tag>
|
</a-space>
|
||||||
</a-space>
|
<a-button class="ant-dns-presets-install" type="primary" @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
|
||||||
</a-col>
|
</div>
|
||||||
<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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,10 @@
|
|||||||
<a-tag v-else color="red">{{ i18n "none" }}</a-tag>
|
<a-tag v-else color="red">{{ i18n "none" }}</a-tag>
|
||||||
<br />
|
<br />
|
||||||
{{ i18n "encryption" }}
|
{{ i18n "encryption" }}
|
||||||
<a-tag :color="inbound.settings.encryption ? 'green' : 'red'">[[ inbound.settings.encryption ? inbound.settings.encryption : '' ]]</a-tag>
|
<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 />
|
<br />
|
||||||
<template v-if="inbound.stream.security != 'none'">
|
<template v-if="inbound.stream.security != 'none'">
|
||||||
{{ i18n "domainName" }}
|
{{ i18n "domainName" }}
|
||||||
|
|||||||
@@ -7,12 +7,13 @@
|
|||||||
<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="65531"></a-input-number>
|
<a-input-number v-model.number="dnsModal.dnsServer.port" :min="1" :max="65535"></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>
|
<a-select-option :value="l" :label="l" v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']"> [[ l ]]
|
||||||
|
</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>
|
||||||
@@ -75,7 +76,7 @@
|
|||||||
isEdit: false,
|
isEdit: false,
|
||||||
confirm: null,
|
confirm: null,
|
||||||
dnsServer: { ...defaultDnsObject },
|
dnsServer: { ...defaultDnsObject },
|
||||||
ok() {
|
ok() {
|
||||||
ObjectUtil.execute(dnsModal.confirm, { ...dnsModal.dnsServer });
|
ObjectUtil.execute(dnsModal.confirm, { ...dnsModal.dnsServer });
|
||||||
},
|
},
|
||||||
show({
|
show({
|
||||||
@@ -106,7 +107,7 @@
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.dnsServer = { ...defaultDnsObject };
|
this.dnsServer = { ...defaultDnsObject };
|
||||||
|
|
||||||
this.dnsServer.domains = [];
|
this.dnsServer.domains = [];
|
||||||
this.dnsServer.expectIPs = [];
|
this.dnsServer.expectIPs = [];
|
||||||
this.dnsServer.unexpectedIPs = [];
|
this.dnsServer.unexpectedIPs = [];
|
||||||
|
|||||||
@@ -9,19 +9,20 @@
|
|||||||
<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" }}'
|
message='{{ i18n "secAlertTitle" }}' color="red" show-icon closable>
|
||||||
color="red"
|
|
||||||
show-icon closable>
|
|
||||||
<template slot="description">
|
<template slot="description">
|
||||||
<b>{{ i18n "secAlertConf" }}</b>
|
<b>{{ i18n "secAlertConf" }}</b>
|
||||||
<ul><li v-for="a in confAlerts">[[ a ]]</li></ul>
|
<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 :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
|
<a-card
|
||||||
|
: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>
|
||||||
@@ -31,17 +32,19 @@
|
|||||||
<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 "pages.settings.save" }}</a-button>
|
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n
|
||||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
|
"pages.settings.save" }}</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')" visibility-height="200"></a-back-top>
|
<a-back-top :target="() => document.getElementById('content-layout')"
|
||||||
|
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" }}'
|
message='{{ i18n "pages.settings.infoDesc" }}' show-icon>
|
||||||
show-icon>
|
|
||||||
</a-alert>
|
</a-alert>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -119,6 +122,7 @@
|
|||||||
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' }],
|
||||||
@@ -131,7 +135,8 @@
|
|||||||
fragment: {
|
fragment: {
|
||||||
packets: "tlshello",
|
packets: "tlshello",
|
||||||
length: "100-200",
|
length: "100-200",
|
||||||
interval: "10-20"
|
interval: "10-20",
|
||||||
|
maxSplit: "300-400"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamSettings: {
|
streamSettings: {
|
||||||
@@ -242,6 +247,17 @@
|
|||||||
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);
|
||||||
@@ -368,6 +384,15 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
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) {
|
||||||
@@ -404,6 +429,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
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 != "";
|
||||||
@@ -534,7 +569,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);
|
||||||
|
|||||||
@@ -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="65531" v-model="allSetting.webPort" :style="{ width: '100%' }"></a-input>
|
<a-input-number :min="1" :min="65535" 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,7 +137,8 @@
|
|||||||
<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" v-model="datepicker">
|
<a-select :style="{ width: '100%' }" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||||
|
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>
|
||||||
@@ -145,5 +146,135 @@
|
|||||||
</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}}
|
||||||
@@ -40,7 +40,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="65531"
|
<a-input-number v-model="allSetting.subPort" :min="1" :min="65535"
|
||||||
:style="{ width: '100%' }"></a-input-number>
|
:style="{ width: '100%' }"></a-input-number>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
@@ -48,7 +48,10 @@
|
|||||||
<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>
|
<a-input type="text" v-model="allSetting.subPath"
|
||||||
|
@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">
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
<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>
|
<a-input type="text" v-model="allSetting.subJsonPath"
|
||||||
|
@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">
|
||||||
@@ -47,6 +50,12 @@
|
|||||||
<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>
|
||||||
@@ -68,7 +77,8 @@
|
|||||||
<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']" :key="p">
|
<a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str', 'hex']"
|
||||||
|
:key="p">
|
||||||
<span>[[ p ]]</span>
|
<span>[[ p ]]</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
|
|||||||
@@ -218,6 +218,8 @@
|
|||||||
<a-menu-item key="android-npvtunnel"
|
<a-menu-item key="android-npvtunnel"
|
||||||
@click="copy(app.subUrl)">NPV
|
@click="copy(app.subUrl)">NPV
|
||||||
Tunnel</a-menu-item>
|
Tunnel</a-menu-item>
|
||||||
|
<a-menu-item key="android-happ"
|
||||||
|
@click="open('happ://add/' + encodeURIComponent(app.subUrl))">Happ</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -244,6 +246,8 @@
|
|||||||
@click="copy(npvtunUrl)">NPV
|
@click="copy(npvtunUrl)">NPV
|
||||||
Tunnel
|
Tunnel
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
<a-menu-item key="ios-happ"
|
||||||
|
@click="open(happUrl)">Happ</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|||||||
@@ -12,13 +12,14 @@
|
|||||||
<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' }" message='{{ i18n "secAlertTitle" }}'
|
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
|
||||||
color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
|
message='{{ i18n "secAlertTitle" }}' 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 :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
|
<a-card
|
||||||
|
: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>
|
||||||
@@ -37,7 +38,8 @@
|
|||||||
<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>
|
<span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line
|
||||||
|
]]</span>
|
||||||
</template>
|
</template>
|
||||||
<a-icon type="question-circle"></a-icon>
|
<a-icon type="question-circle"></a-icon>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
@@ -534,13 +536,12 @@
|
|||||||
serverObj = null;
|
serverObj = null;
|
||||||
switch (o.protocol) {
|
switch (o.protocol) {
|
||||||
case Protocols.VMess:
|
case Protocols.VMess:
|
||||||
case Protocols.VLESS:
|
serverObj = o.settings.vnext;
|
||||||
if (o.settings && o.settings.address && o.settings.port) {
|
|
||||||
return [o.settings.address + ':' + o.settings.port];
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
case Protocols.VLESS:
|
||||||
|
return [o.settings?.address + ':' + o.settings?.port];
|
||||||
case Protocols.HTTP:
|
case Protocols.HTTP:
|
||||||
case Protocols.Mixed:
|
case Protocols.Socks:
|
||||||
case Protocols.Shadowsocks:
|
case Protocols.Shadowsocks:
|
||||||
case Protocols.Trojan:
|
case Protocols.Trojan:
|
||||||
serverObj = o.settings.servers;
|
serverObj = o.settings.servers;
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"github.com/mhsanaei/3x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/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
|
||||||
@@ -25,6 +26,7 @@ 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
|
||||||
|
|||||||
@@ -4,21 +4,23 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here run is a interface method of Job interface
|
// Run checks CPU usage over the last minute and sends a Telegram alert if it exceeds the threshold.
|
||||||
func (j *CheckCpuJob) Run() {
|
func (j *CheckCpuJob) Run() {
|
||||||
threshold, _ := j.settingService.GetTgCpu()
|
threshold, _ := j.settingService.GetTgCpu()
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here Run is an interface method of the Job interface
|
// Run removes expired hash entries from the Telegram bot's hash storage.
|
||||||
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()
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
|
// 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/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/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
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/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)
|
||||||
}
|
}
|
||||||
|
|||||||
421
web/job/ldap_sync_job.go
Normal file
421
web/job/ldap_sync_job.go
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
package job
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
|
ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DefaultTruthyValues = []string{"true", "1", "yes", "on"}
|
||||||
|
|
||||||
|
type LdapSyncJob struct {
|
||||||
|
settingService service.SettingService
|
||||||
|
inboundService service.InboundService
|
||||||
|
xrayService service.XrayService
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper functions for mustGet ---
|
||||||
|
func mustGetString(fn func() (string, error)) string {
|
||||||
|
v, err := fn()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetInt(fn func() (int, error)) int {
|
||||||
|
v, err := fn()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetBool(fn func() (bool, error)) bool {
|
||||||
|
v, err := fn()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetStringOr(fn func() (string, error), fallback string) string {
|
||||||
|
v, err := fn()
|
||||||
|
if err != nil || v == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLdapSyncJob() *LdapSyncJob {
|
||||||
|
return new(LdapSyncJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *LdapSyncJob) Run() {
|
||||||
|
logger.Info("LDAP sync job started")
|
||||||
|
|
||||||
|
enabled, err := j.settingService.GetLdapEnable()
|
||||||
|
if err != nil || !enabled {
|
||||||
|
logger.Warning("LDAP disabled or failed to fetch flag")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LDAP fetch ---
|
||||||
|
cfg := ldaputil.Config{
|
||||||
|
Host: mustGetString(j.settingService.GetLdapHost),
|
||||||
|
Port: mustGetInt(j.settingService.GetLdapPort),
|
||||||
|
UseTLS: mustGetBool(j.settingService.GetLdapUseTLS),
|
||||||
|
BindDN: mustGetString(j.settingService.GetLdapBindDN),
|
||||||
|
Password: mustGetString(j.settingService.GetLdapPassword),
|
||||||
|
BaseDN: mustGetString(j.settingService.GetLdapBaseDN),
|
||||||
|
UserFilter: mustGetString(j.settingService.GetLdapUserFilter),
|
||||||
|
UserAttr: mustGetString(j.settingService.GetLdapUserAttr),
|
||||||
|
FlagField: mustGetStringOr(j.settingService.GetLdapFlagField, mustGetString(j.settingService.GetLdapVlessField)),
|
||||||
|
TruthyVals: splitCsv(mustGetString(j.settingService.GetLdapTruthyValues)),
|
||||||
|
Invert: mustGetBool(j.settingService.GetLdapInvertFlag),
|
||||||
|
}
|
||||||
|
|
||||||
|
flags, err := ldaputil.FetchVlessFlags(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("LDAP fetch failed:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Infof("Fetched %d LDAP flags", len(flags))
|
||||||
|
|
||||||
|
// --- Load all inbounds and all clients once ---
|
||||||
|
inboundTags := splitCsv(mustGetString(j.settingService.GetLdapInboundTags))
|
||||||
|
inbounds, err := j.inboundService.GetAllInbounds()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Failed to get inbounds:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allClients := map[string]*model.Client{} // email -> client
|
||||||
|
inboundMap := map[string]*model.Inbound{} // tag -> inbound
|
||||||
|
for _, ib := range inbounds {
|
||||||
|
inboundMap[ib.Tag] = ib
|
||||||
|
clients, _ := j.inboundService.GetClients(ib)
|
||||||
|
for i := range clients {
|
||||||
|
allClients[clients[i].Email] = &clients[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Prepare batch operations ---
|
||||||
|
autoCreate := mustGetBool(j.settingService.GetLdapAutoCreate)
|
||||||
|
defGB := mustGetInt(j.settingService.GetLdapDefaultTotalGB)
|
||||||
|
defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays)
|
||||||
|
defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP)
|
||||||
|
|
||||||
|
clientsToCreate := map[string][]model.Client{} // tag -> []new clients
|
||||||
|
clientsToEnable := map[string][]string{} // tag -> []email
|
||||||
|
clientsToDisable := map[string][]string{} // tag -> []email
|
||||||
|
|
||||||
|
for email, allowed := range flags {
|
||||||
|
exists := allClients[email] != nil
|
||||||
|
for _, tag := range inboundTags {
|
||||||
|
if !exists && allowed && autoCreate {
|
||||||
|
newClient := j.buildClient(inboundMap[tag], email, defGB, defExpiryDays, defLimitIP)
|
||||||
|
clientsToCreate[tag] = append(clientsToCreate[tag], newClient)
|
||||||
|
} else if exists {
|
||||||
|
if allowed && !allClients[email].Enable {
|
||||||
|
clientsToEnable[tag] = append(clientsToEnable[tag], email)
|
||||||
|
} else if !allowed && allClients[email].Enable {
|
||||||
|
clientsToDisable[tag] = append(clientsToDisable[tag], email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Execute batch create ---
|
||||||
|
for tag, newClients := range clientsToCreate {
|
||||||
|
if len(newClients) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
payload := &model.Inbound{Id: inboundMap[tag].Id}
|
||||||
|
payload.Settings = j.clientsToJSON(newClients)
|
||||||
|
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
|
||||||
|
logger.Warningf("Failed to add clients for tag %s: %v", tag, err)
|
||||||
|
} else {
|
||||||
|
logger.Infof("LDAP auto-create: %d clients for %s", len(newClients), tag)
|
||||||
|
j.xrayService.SetToNeedRestart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Execute enable/disable batch ---
|
||||||
|
for tag, emails := range clientsToEnable {
|
||||||
|
j.batchSetEnable(inboundMap[tag], emails, true)
|
||||||
|
}
|
||||||
|
for tag, emails := range clientsToDisable {
|
||||||
|
j.batchSetEnable(inboundMap[tag], emails, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Auto delete clients not in LDAP ---
|
||||||
|
autoDelete := mustGetBool(j.settingService.GetLdapAutoDelete)
|
||||||
|
if autoDelete {
|
||||||
|
ldapEmailSet := map[string]struct{}{}
|
||||||
|
for e := range flags {
|
||||||
|
ldapEmailSet[e] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, tag := range inboundTags {
|
||||||
|
j.deleteClientsNotInLDAP(tag, ldapEmailSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitCsv(s string) []string {
|
||||||
|
if s == "" {
|
||||||
|
return DefaultTruthyValues
|
||||||
|
}
|
||||||
|
parts := strings.Split(s, ",")
|
||||||
|
out := make([]string, 0, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
v := strings.TrimSpace(p)
|
||||||
|
if v != "" {
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildClient creates a new client for auto-create
|
||||||
|
func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client {
|
||||||
|
c := model.Client{
|
||||||
|
Email: email,
|
||||||
|
Enable: true,
|
||||||
|
LimitIP: defLimitIP,
|
||||||
|
TotalGB: int64(defGB),
|
||||||
|
}
|
||||||
|
if defExpiryDays > 0 {
|
||||||
|
c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
|
||||||
|
}
|
||||||
|
switch ib.Protocol {
|
||||||
|
case model.Trojan, model.Shadowsocks:
|
||||||
|
c.Password = uuid.NewString()
|
||||||
|
default:
|
||||||
|
c.ID = uuid.NewString()
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// batchSetEnable enables/disables clients in batch through a single call
|
||||||
|
func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) {
|
||||||
|
if len(emails) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare JSON for mass update
|
||||||
|
clients := make([]model.Client, 0, len(emails))
|
||||||
|
for _, email := range emails {
|
||||||
|
clients = append(clients, model.Client{
|
||||||
|
Email: email,
|
||||||
|
Enable: enable,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := &model.Inbound{
|
||||||
|
Id: ib.Id,
|
||||||
|
Settings: j.clientsToJSON(clients),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a single AddInboundClient call to update enable
|
||||||
|
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
|
||||||
|
logger.Warningf("Batch set enable failed for inbound %s: %v", ib.Tag, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, len(emails), ib.Tag)
|
||||||
|
j.xrayService.SetToNeedRestart()
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart
|
||||||
|
func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) {
|
||||||
|
inbounds, err := j.inboundService.GetAllInbounds()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Failed to get inbounds for deletion:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
batchSize := 50 // clients in 1 batch
|
||||||
|
restartNeeded := false
|
||||||
|
|
||||||
|
for _, ib := range inbounds {
|
||||||
|
if ib.Tag != inboundTag {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clients, err := j.inboundService.GetClients(ib)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("Failed to get clients for inbound %s: %v", ib.Tag, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect clients for deletion
|
||||||
|
toDelete := []model.Client{}
|
||||||
|
for _, c := range clients {
|
||||||
|
if _, ok := ldapEmails[c.Email]; !ok {
|
||||||
|
toDelete = append(toDelete, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(toDelete) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete in batches
|
||||||
|
for i := 0; i < len(toDelete); i += batchSize {
|
||||||
|
end := i + batchSize
|
||||||
|
if end > len(toDelete) {
|
||||||
|
end = len(toDelete)
|
||||||
|
}
|
||||||
|
batch := toDelete[i:end]
|
||||||
|
|
||||||
|
for _, c := range batch {
|
||||||
|
var clientKey string
|
||||||
|
switch ib.Protocol {
|
||||||
|
case model.Trojan:
|
||||||
|
clientKey = c.Password
|
||||||
|
case model.Shadowsocks:
|
||||||
|
clientKey = c.Email
|
||||||
|
default: // vless/vmess
|
||||||
|
clientKey = c.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := j.inboundService.DelInboundClient(ib.Id, clientKey); err != nil {
|
||||||
|
logger.Warningf("Failed to delete client %s from inbound id=%d(tag=%s): %v",
|
||||||
|
c.Email, ib.Id, ib.Tag, err)
|
||||||
|
} else {
|
||||||
|
logger.Infof("Deleted client %s from inbound id=%d(tag=%s)",
|
||||||
|
c.Email, ib.Id, ib.Tag)
|
||||||
|
// do not restart here
|
||||||
|
restartNeeded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One time after all batches
|
||||||
|
if restartNeeded {
|
||||||
|
j.xrayService.SetToNeedRestart()
|
||||||
|
logger.Info("Xray restart scheduled after batch deletion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clientsToJSON serializes an array of clients to JSON
|
||||||
|
func (j *LdapSyncJob) clientsToJSON(clients []model.Client) string {
|
||||||
|
b := strings.Builder{}
|
||||||
|
b.WriteString("{\"clients\":[")
|
||||||
|
for i, c := range clients {
|
||||||
|
if i > 0 {
|
||||||
|
b.WriteString(",")
|
||||||
|
}
|
||||||
|
b.WriteString(j.clientToJSON(c))
|
||||||
|
}
|
||||||
|
b.WriteString("]}")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureClientExists adds client with defaults to inbound tag if not present
|
||||||
|
func (j *LdapSyncJob) ensureClientExists(inboundTag string, email string, defGB int, defExpiryDays int, defLimitIP int) {
|
||||||
|
inbounds, err := j.inboundService.GetAllInbounds()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("ensureClientExists: get inbounds failed:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var target *model.Inbound
|
||||||
|
for _, ib := range inbounds {
|
||||||
|
if ib.Tag == inboundTag {
|
||||||
|
target = ib
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if target == nil {
|
||||||
|
logger.Debugf("ensureClientExists: inbound tag %s not found", inboundTag)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// check if email already exists in this inbound
|
||||||
|
clients, err := j.inboundService.GetClients(target)
|
||||||
|
if err == nil {
|
||||||
|
for _, c := range clients {
|
||||||
|
if c.Email == email {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// build new client according to protocol
|
||||||
|
newClient := model.Client{
|
||||||
|
Email: email,
|
||||||
|
Enable: true,
|
||||||
|
LimitIP: defLimitIP,
|
||||||
|
TotalGB: int64(defGB),
|
||||||
|
}
|
||||||
|
if defExpiryDays > 0 {
|
||||||
|
newClient.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch target.Protocol {
|
||||||
|
case model.Trojan:
|
||||||
|
newClient.Password = uuid.NewString()
|
||||||
|
case model.Shadowsocks:
|
||||||
|
newClient.Password = uuid.NewString()
|
||||||
|
default: // VMESS/VLESS and others using ID
|
||||||
|
newClient.ID = uuid.NewString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare inbound payload with only the new client
|
||||||
|
payload := &model.Inbound{Id: target.Id}
|
||||||
|
payload.Settings = `{"clients":[` + j.clientToJSON(newClient) + `]}`
|
||||||
|
|
||||||
|
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
|
||||||
|
logger.Warning("ensureClientExists: add client failed:", err)
|
||||||
|
} else {
|
||||||
|
j.xrayService.SetToNeedRestart()
|
||||||
|
logger.Infof("LDAP auto-create: %s in %s", email, inboundTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clientToJSON serializes minimal client fields to JSON object string without extra deps
|
||||||
|
func (j *LdapSyncJob) clientToJSON(c model.Client) string {
|
||||||
|
// construct minimal JSON manually to avoid importing json for simple case
|
||||||
|
b := strings.Builder{}
|
||||||
|
b.WriteString("{")
|
||||||
|
if c.ID != "" {
|
||||||
|
b.WriteString("\"id\":\"")
|
||||||
|
b.WriteString(c.ID)
|
||||||
|
b.WriteString("\",")
|
||||||
|
}
|
||||||
|
if c.Password != "" {
|
||||||
|
b.WriteString("\"password\":\"")
|
||||||
|
b.WriteString(c.Password)
|
||||||
|
b.WriteString("\",")
|
||||||
|
}
|
||||||
|
b.WriteString("\"email\":\"")
|
||||||
|
b.WriteString(c.Email)
|
||||||
|
b.WriteString("\",")
|
||||||
|
b.WriteString("\"enable\":")
|
||||||
|
if c.Enable {
|
||||||
|
b.WriteString("true")
|
||||||
|
} else {
|
||||||
|
b.WriteString("false")
|
||||||
|
}
|
||||||
|
b.WriteString(",")
|
||||||
|
b.WriteString("\"limitIp\":")
|
||||||
|
b.WriteString(strconv.Itoa(c.LimitIP))
|
||||||
|
b.WriteString(",")
|
||||||
|
b.WriteString("\"totalGB\":")
|
||||||
|
b.WriteString(strconv.FormatInt(c.TotalGB, 10))
|
||||||
|
if c.ExpiryTime > 0 {
|
||||||
|
b.WriteString(",\"expiryTime\":")
|
||||||
|
b.WriteString(strconv.FormatInt(c.ExpiryTime, 10))
|
||||||
|
}
|
||||||
|
b.WriteString("}")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
@@ -1,23 +1,27 @@
|
|||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Period represents the time period for traffic resets.
|
||||||
type Period string
|
type Period string
|
||||||
|
|
||||||
|
// PeriodicTrafficResetJob resets traffic statistics for inbounds based on their configured reset period.
|
||||||
type PeriodicTrafficResetJob struct {
|
type PeriodicTrafficResetJob struct {
|
||||||
inboundService service.InboundService
|
inboundService service.InboundService
|
||||||
period Period
|
period Period
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewPeriodicTrafficResetJob creates a new periodic traffic reset job for the specified period.
|
||||||
func NewPeriodicTrafficResetJob(period Period) *PeriodicTrafficResetJob {
|
func NewPeriodicTrafficResetJob(period Period) *PeriodicTrafficResetJob {
|
||||||
return &PeriodicTrafficResetJob{
|
return &PeriodicTrafficResetJob{
|
||||||
period: period,
|
period: period,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run resets traffic statistics for all inbounds that match the configured reset period.
|
||||||
func (j *PeriodicTrafficResetJob) Run() {
|
func (j *PeriodicTrafficResetJob) Run() {
|
||||||
inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))
|
inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -33,13 +37,19 @@ func (j *PeriodicTrafficResetJob) Run() {
|
|||||||
resetCount := 0
|
resetCount := 0
|
||||||
|
|
||||||
for _, inbound := range inbounds {
|
for _, inbound := range inbounds {
|
||||||
if err := j.inboundService.ResetAllClientTraffics(inbound.Id); err != nil {
|
resetInboundErr := j.inboundService.ResetAllTraffics()
|
||||||
logger.Warning("Failed to reset traffic for inbound", inbound.Id, ":", err)
|
if resetInboundErr != nil {
|
||||||
continue
|
logger.Warning("Failed to reset traffic for inbound", inbound.Id, ":", resetInboundErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
resetCount++
|
resetClientErr := j.inboundService.ResetAllClientTraffics(inbound.Id)
|
||||||
logger.Infof("Reset traffic for inbound %d (%s)", inbound.Id, inbound.Remark)
|
if resetClientErr != nil {
|
||||||
|
logger.Warning("Failed to reset traffic for all users of inbound", inbound.Id, ":", resetClientErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resetInboundErr == nil && resetClientErr == nil {
|
||||||
|
resetCount++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if resetCount > 0 {
|
if resetCount > 0 {
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LoginStatus represents the status of a login attempt.
|
||||||
type LoginStatus byte
|
type LoginStatus byte
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LoginSuccess LoginStatus = 1
|
LoginSuccess LoginStatus = 1 // Successful login
|
||||||
LoginFail LoginStatus = 0
|
LoginFail LoginStatus = 0 // Failed login attempt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// StatsNotifyJob sends periodic statistics reports via Telegram bot.
|
||||||
type StatsNotifyJob struct {
|
type StatsNotifyJob struct {
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
tgbotService service.Tgbot
|
tgbotService service.Tgbot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewStatsNotifyJob creates a new statistics notification job instance.
|
||||||
func NewStatsNotifyJob() *StatsNotifyJob {
|
func NewStatsNotifyJob() *StatsNotifyJob {
|
||||||
return new(StatsNotifyJob)
|
return new(StatsNotifyJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here run is a interface method of Job interface
|
// Run sends a statistics report via Telegram bot if Xray is running.
|
||||||
func (j *StatsNotifyJob) Run() {
|
func (j *StatsNotifyJob) Run() {
|
||||||
if !j.xrayService.IsXrayRunning() {
|
if !j.xrayService.IsXrayRunning() {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ package job
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
"github.com/mhsanaei/3x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// XrayTrafficJob collects and processes traffic statistics from Xray, updating the database and optionally informing external APIs.
|
||||||
type XrayTrafficJob struct {
|
type XrayTrafficJob struct {
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
@@ -17,10 +18,12 @@ type XrayTrafficJob struct {
|
|||||||
outboundService service.OutboundService
|
outboundService service.OutboundService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewXrayTrafficJob creates a new traffic collection job instance.
|
||||||
func NewXrayTrafficJob() *XrayTrafficJob {
|
func NewXrayTrafficJob() *XrayTrafficJob {
|
||||||
return new(XrayTrafficJob)
|
return new(XrayTrafficJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run collects traffic statistics from Xray and updates the database, triggering restart if needed.
|
||||||
func (j *XrayTrafficJob) Run() {
|
func (j *XrayTrafficJob) Run() {
|
||||||
if !j.xrayService.IsXrayRunning() {
|
if !j.xrayService.IsXrayRunning() {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Package locale provides internationalization (i18n) support for the 3x-ui web panel,
|
||||||
|
// including translation loading, localization, and middleware for web and bot interfaces.
|
||||||
package locale
|
package locale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -6,7 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
@@ -20,17 +22,20 @@ var (
|
|||||||
LocalizerBot *i18n.Localizer
|
LocalizerBot *i18n.Localizer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// I18nType represents the type of interface for internationalization.
|
||||||
type I18nType string
|
type I18nType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Bot I18nType = "bot"
|
Bot I18nType = "bot" // Bot interface type
|
||||||
Web I18nType = "web"
|
Web I18nType = "web" // Web interface type
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SettingService interface defines methods for accessing locale settings.
|
||||||
type SettingService interface {
|
type SettingService interface {
|
||||||
GetTgLang() (string, error)
|
GetTgLang() (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitLocalizer initializes the internationalization system with embedded translation files.
|
||||||
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
||||||
// set default bundle to english
|
// set default bundle to english
|
||||||
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
|
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
|
||||||
@@ -49,10 +54,11 @@ func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTemplateData(params []string, seperator ...string) map[string]any {
|
// createTemplateData creates a template data map from parameters with optional separator.
|
||||||
|
func createTemplateData(params []string, separator ...string) map[string]any {
|
||||||
var sep string = "=="
|
var sep string = "=="
|
||||||
if len(seperator) > 0 {
|
if len(separator) > 0 {
|
||||||
sep = seperator[0]
|
sep = separator[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
templateData := make(map[string]any)
|
templateData := make(map[string]any)
|
||||||
@@ -64,6 +70,9 @@ func createTemplateData(params []string, seperator ...string) map[string]any {
|
|||||||
return templateData
|
return templateData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// I18n retrieves a localized message for the given key and type.
|
||||||
|
// It supports both bot and web contexts, with optional template parameters.
|
||||||
|
// Returns the localized message or an empty string if localization fails.
|
||||||
func I18n(i18nType I18nType, key string, params ...string) string {
|
func I18n(i18nType I18nType, key string, params ...string) string {
|
||||||
var localizer *i18n.Localizer
|
var localizer *i18n.Localizer
|
||||||
|
|
||||||
@@ -96,6 +105,7 @@ func I18n(i18nType I18nType, key string, params ...string) string {
|
|||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initTGBotLocalizer initializes the bot localizer with the configured language.
|
||||||
func initTGBotLocalizer(settingService SettingService) error {
|
func initTGBotLocalizer(settingService SettingService) error {
|
||||||
botLang, err := settingService.GetTgLang()
|
botLang, err := settingService.GetTgLang()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -106,6 +116,10 @@ func initTGBotLocalizer(settingService SettingService) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LocalizerMiddleware returns a Gin middleware that sets up localization for web requests.
|
||||||
|
// It determines the user's language from cookies or Accept-Language header,
|
||||||
|
// creates a localizer instance, and stores it in the Gin context for use in handlers.
|
||||||
|
// Also provides the I18n function in the context for template rendering.
|
||||||
func LocalizerMiddleware() gin.HandlerFunc {
|
func LocalizerMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// Ensure bundle is initialized so creating a Localizer won't panic
|
// Ensure bundle is initialized so creating a Localizer won't panic
|
||||||
@@ -152,6 +166,7 @@ func loadTranslationsFromDisk(bundle *i18n.Bundle) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseTranslationFiles parses embedded translation files and adds them to the i18n bundle.
|
||||||
func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
|
func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
|
||||||
err := fs.WalkDir(i18nFS, "translation",
|
err := fs.WalkDir(i18nFS, "translation",
|
||||||
func(path string, d fs.DirEntry, err error) error {
|
func(path string, d fs.DirEntry, err error) error {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Package middleware provides HTTP middleware functions for the 3x-ui web panel,
|
||||||
|
// including domain validation and URL redirection utilities.
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -8,6 +10,10 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DomainValidatorMiddleware returns a Gin middleware that validates the request domain.
|
||||||
|
// It extracts the host from the request, strips any port number, and compares it
|
||||||
|
// against the configured domain. Requests from unauthorized domains are rejected
|
||||||
|
// with HTTP 403 Forbidden status.
|
||||||
func DomainValidatorMiddleware(domain string) gin.HandlerFunc {
|
func DomainValidatorMiddleware(domain string) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
host := c.Request.Host
|
host := c.Request.Host
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RedirectMiddleware returns a Gin middleware that handles URL redirections.
|
||||||
|
// It provides backward compatibility by redirecting old '/xui' paths to new '/panel' paths,
|
||||||
|
// including API endpoints. The middleware performs permanent redirects (301) for SEO purposes.
|
||||||
func RedirectMiddleware(basePath string) gin.HandlerFunc {
|
func RedirectMiddleware(basePath string) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// Redirect from old '/xui' path to '/panel'
|
// Redirect from old '/xui' path to '/panel'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Package network provides network utilities for the 3x-ui web panel,
|
||||||
|
// including automatic HTTP to HTTPS redirection functionality.
|
||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -9,6 +11,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AutoHttpsConn wraps a net.Conn to provide automatic HTTP to HTTPS redirection.
|
||||||
|
// It intercepts the first read to detect HTTP requests and responds with a 307 redirect
|
||||||
|
// to the HTTPS equivalent URL. Subsequent reads work normally for HTTPS connections.
|
||||||
type AutoHttpsConn struct {
|
type AutoHttpsConn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
|
|
||||||
@@ -18,6 +23,8 @@ type AutoHttpsConn struct {
|
|||||||
readRequestOnce sync.Once
|
readRequestOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewAutoHttpsConn creates a new AutoHttpsConn that wraps the given connection.
|
||||||
|
// It enables automatic redirection of HTTP requests to HTTPS.
|
||||||
func NewAutoHttpsConn(conn net.Conn) net.Conn {
|
func NewAutoHttpsConn(conn net.Conn) net.Conn {
|
||||||
return &AutoHttpsConn{
|
return &AutoHttpsConn{
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
@@ -49,6 +56,9 @@ func (c *AutoHttpsConn) readRequest() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read implements the net.Conn Read method with automatic HTTPS redirection.
|
||||||
|
// On the first read, it checks if the request is HTTP and redirects to HTTPS if so.
|
||||||
|
// Subsequent reads work normally.
|
||||||
func (c *AutoHttpsConn) Read(buf []byte) (int, error) {
|
func (c *AutoHttpsConn) Read(buf []byte) (int, error) {
|
||||||
c.readRequestOnce.Do(func() {
|
c.readRequestOnce.Do(func() {
|
||||||
c.readRequest()
|
c.readRequest()
|
||||||
|
|||||||
@@ -2,16 +2,22 @@ package network
|
|||||||
|
|
||||||
import "net"
|
import "net"
|
||||||
|
|
||||||
|
// AutoHttpsListener wraps a net.Listener to provide automatic HTTPS redirection.
|
||||||
|
// It returns AutoHttpsConn connections that handle HTTP to HTTPS redirection.
|
||||||
type AutoHttpsListener struct {
|
type AutoHttpsListener struct {
|
||||||
net.Listener
|
net.Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewAutoHttpsListener creates a new AutoHttpsListener that wraps the given listener.
|
||||||
|
// It enables automatic redirection of HTTP requests to HTTPS for all accepted connections.
|
||||||
func NewAutoHttpsListener(listener net.Listener) net.Listener {
|
func NewAutoHttpsListener(listener net.Listener) net.Listener {
|
||||||
return &AutoHttpsListener{
|
return &AutoHttpsListener{
|
||||||
Listener: listener,
|
Listener: listener,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accept implements the net.Listener Accept method.
|
||||||
|
// It accepts connections and wraps them with AutoHttpsConn for HTTPS redirection.
|
||||||
func (l *AutoHttpsListener) Accept() (net.Conn, error) {
|
func (l *AutoHttpsListener) Accept() (net.Conn, error) {
|
||||||
conn, err := l.Listener.Accept()
|
conn, err := l.Listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Package service provides business logic services for the 3x-ui web panel,
|
||||||
|
// including inbound/outbound management, user administration, settings, and Xray integration.
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -8,19 +10,24 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"github.com/mhsanaei/3x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"github.com/mhsanaei/3x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// InboundService provides business logic for managing Xray inbound configurations.
|
||||||
|
// It handles CRUD operations for inbounds, client management, traffic monitoring,
|
||||||
|
// and integration with the Xray API for real-time updates.
|
||||||
type InboundService struct {
|
type InboundService struct {
|
||||||
xrayApi xray.XrayAPI
|
xrayApi xray.XrayAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInbounds retrieves all inbounds for a specific user.
|
||||||
|
// Returns a slice of inbound models with their associated client statistics.
|
||||||
func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
|
func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var inbounds []*model.Inbound
|
var inbounds []*model.Inbound
|
||||||
@@ -28,9 +35,30 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
|
|||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Enrich client stats with UUID/SubId from inbound settings
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
clients, _ := s.GetClients(inbound)
|
||||||
|
if len(clients) == 0 || len(inbound.ClientStats) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Build a map email -> client
|
||||||
|
cMap := make(map[string]model.Client, len(clients))
|
||||||
|
for _, c := range clients {
|
||||||
|
cMap[strings.ToLower(c.Email)] = c
|
||||||
|
}
|
||||||
|
for i := range inbound.ClientStats {
|
||||||
|
email := strings.ToLower(inbound.ClientStats[i].Email)
|
||||||
|
if c, ok := cMap[email]; ok {
|
||||||
|
inbound.ClientStats[i].UUID = c.ID
|
||||||
|
inbound.ClientStats[i].SubId = c.SubID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return inbounds, nil
|
return inbounds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllInbounds retrieves all inbounds from the database.
|
||||||
|
// Returns a slice of all inbound models with their associated client statistics.
|
||||||
func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
|
func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var inbounds []*model.Inbound
|
var inbounds []*model.Inbound
|
||||||
@@ -38,6 +66,24 @@ func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
|
|||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Enrich client stats with UUID/SubId from inbound settings
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
clients, _ := s.GetClients(inbound)
|
||||||
|
if len(clients) == 0 || len(inbound.ClientStats) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cMap := make(map[string]model.Client, len(clients))
|
||||||
|
for _, c := range clients {
|
||||||
|
cMap[strings.ToLower(c.Email)] = c
|
||||||
|
}
|
||||||
|
for i := range inbound.ClientStats {
|
||||||
|
email := strings.ToLower(inbound.ClientStats[i].Email)
|
||||||
|
if c, ok := cMap[email]; ok {
|
||||||
|
inbound.ClientStats[i].UUID = c.ID
|
||||||
|
inbound.ClientStats[i].SubId = c.SubID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return inbounds, nil
|
return inbounds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +209,10 @@ func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (stri
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddInbound creates a new inbound configuration.
|
||||||
|
// It validates port uniqueness, client email uniqueness, and required fields,
|
||||||
|
// then saves the inbound to the database and optionally adds it to the running Xray instance.
|
||||||
|
// Returns the created inbound, whether Xray needs restart, and any error.
|
||||||
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
||||||
exist, err := s.checkPortExist(inbound.Listen, inbound.Port, 0)
|
exist, err := s.checkPortExist(inbound.Listen, inbound.Port, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -269,6 +319,9 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
|||||||
return inbound, needRestart, err
|
return inbound, needRestart, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DelInbound deletes an inbound configuration by ID.
|
||||||
|
// It removes the inbound from the database and the running Xray instance if active.
|
||||||
|
// Returns whether Xray needs restart and any error.
|
||||||
func (s *InboundService) DelInbound(id int) (bool, error) {
|
func (s *InboundService) DelInbound(id int) (bool, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
@@ -322,6 +375,9 @@ func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
|
|||||||
return inbound, nil
|
return inbound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateInbound modifies an existing inbound configuration.
|
||||||
|
// It validates changes, updates the database, and syncs with the running Xray instance.
|
||||||
|
// Returns the updated inbound, whether Xray needs restart, and any error.
|
||||||
func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
||||||
exist, err := s.checkPortExist(inbound.Listen, inbound.Port, inbound.Id)
|
exist, err := s.checkPortExist(inbound.Listen, inbound.Port, inbound.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1513,6 +1569,23 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
|
|||||||
return !clientOldEnabled, needRestart, nil
|
return !clientOldEnabled, needRestart, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// SetClientEnableByEmail sets client enable state to desired value; returns (changed, needRestart, error)
|
||||||
|
func (s *InboundService) SetClientEnableByEmail(clientEmail string, enable bool) (bool, bool, error) {
|
||||||
|
current, err := s.checkIsEnabledByEmail(clientEmail)
|
||||||
|
if err != nil {
|
||||||
|
return false, false, err
|
||||||
|
}
|
||||||
|
if current == enable {
|
||||||
|
return false, false, nil
|
||||||
|
}
|
||||||
|
newEnabled, needRestart, err := s.ToggleClientEnableByEmail(clientEmail)
|
||||||
|
if err != nil {
|
||||||
|
return false, needRestart, err
|
||||||
|
}
|
||||||
|
return newEnabled == enable, needRestart, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) (bool, error) {
|
func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) (bool, error) {
|
||||||
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
|
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1940,6 +2013,15 @@ func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffi
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Populate UUID and other client data for each traffic record
|
||||||
|
for i := range traffics {
|
||||||
|
if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil {
|
||||||
|
traffics[i].Enable = client.Enable
|
||||||
|
traffics[i].UUID = client.ID
|
||||||
|
traffics[i].SubId = client.SubID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return traffics, nil
|
return traffics, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1952,6 +2034,7 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
|
|||||||
}
|
}
|
||||||
if t != nil && client != nil {
|
if t != nil && client != nil {
|
||||||
t.Enable = client.Enable
|
t.Enable = client.Enable
|
||||||
|
t.UUID = client.ID
|
||||||
t.SubId = client.SubID
|
t.SubId = client.SubID
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
@@ -1993,6 +2076,7 @@ func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic,
|
|||||||
for i := range traffics {
|
for i := range traffics {
|
||||||
if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil {
|
if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil {
|
||||||
traffics[i].Enable = client.Enable
|
traffics[i].Enable = client.Enable
|
||||||
|
traffics[i].UUID = client.ID
|
||||||
traffics[i].SubId = client.SubID
|
traffics[i].SubId = client.SubID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2091,6 +2175,9 @@ func (s *InboundService) MigrationRequirements() {
|
|||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
|
if dbErr := db.Exec(`VACUUM "main"`).Error; dbErr != nil {
|
||||||
|
logger.Warningf("VACUUM failed: %v", dbErr)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/mhsanaei/3x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"github.com/mhsanaei/3x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// OutboundService provides business logic for managing Xray outbound configurations.
|
||||||
|
// It handles outbound traffic monitoring and statistics.
|
||||||
type OutboundService struct{}
|
type OutboundService struct{}
|
||||||
|
|
||||||
func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
|
func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PanelService provides business logic for panel management operations.
|
||||||
|
// It handles panel restart, updates, and system-level panel controls.
|
||||||
type PanelService struct{}
|
type PanelService struct{}
|
||||||
|
|
||||||
func (s *PanelService) RestartPanel(delay time.Duration) error {
|
func (s *PanelService) RestartPanel(delay time.Duration) error {
|
||||||
|
|||||||
@@ -13,18 +13,19 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"github.com/mhsanaei/3x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"github.com/mhsanaei/3x-ui/util/sys"
|
"github.com/mhsanaei/3x-ui/v2/util/sys"
|
||||||
"github.com/mhsanaei/3x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/shirou/gopsutil/v4/cpu"
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
@@ -35,14 +36,18 @@ import (
|
|||||||
"github.com/shirou/gopsutil/v4/net"
|
"github.com/shirou/gopsutil/v4/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ProcessState represents the current state of a system process.
|
||||||
type ProcessState string
|
type ProcessState string
|
||||||
|
|
||||||
|
// Process state constants
|
||||||
const (
|
const (
|
||||||
Running ProcessState = "running"
|
Running ProcessState = "running" // Process is running normally
|
||||||
Stop ProcessState = "stop"
|
Stop ProcessState = "stop" // Process is stopped
|
||||||
Error ProcessState = "error"
|
Error ProcessState = "error" // Process is in error state
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Status represents comprehensive system and application status information.
|
||||||
|
// It includes CPU, memory, disk, network statistics, and Xray process status.
|
||||||
type Status struct {
|
type Status struct {
|
||||||
T time.Time `json:"-"`
|
T time.Time `json:"-"`
|
||||||
Cpu float64 `json:"cpu"`
|
Cpu float64 `json:"cpu"`
|
||||||
@@ -89,10 +94,13 @@ type Status struct {
|
|||||||
} `json:"appStats"`
|
} `json:"appStats"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Release represents information about a software release from GitHub.
|
||||||
type Release struct {
|
type Release struct {
|
||||||
TagName string `json:"tag_name"`
|
TagName string `json:"tag_name"` // The tag name of the release
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServerService provides business logic for server monitoring and management.
|
||||||
|
// It handles system status collection, IP detection, and application statistics.
|
||||||
type ServerService struct {
|
type ServerService struct {
|
||||||
xrayService XrayService
|
xrayService XrayService
|
||||||
inboundService InboundService
|
inboundService InboundService
|
||||||
@@ -102,6 +110,7 @@ type ServerService struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
lastCPUTimes cpu.TimesStat
|
lastCPUTimes cpu.TimesStat
|
||||||
hasLastCPUSample bool
|
hasLastCPUSample bool
|
||||||
|
hasNativeCPUSample bool
|
||||||
emaCPU float64
|
emaCPU float64
|
||||||
cpuHistory []CPUSample
|
cpuHistory []CPUSample
|
||||||
cachedCpuSpeedMhz float64
|
cachedCpuSpeedMhz float64
|
||||||
@@ -424,23 +433,27 @@ func (s *ServerService) AppendCpuSample(t time.Time, v float64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerService) sampleCPUUtilization() (float64, error) {
|
func (s *ServerService) sampleCPUUtilization() (float64, error) {
|
||||||
// Prefer native Windows API to avoid external deps for CPU percent
|
// Try native platform-specific CPU implementation first (Windows, Linux, macOS)
|
||||||
if runtime.GOOS == "windows" {
|
if pct, err := sys.CPUPercentRaw(); err == nil {
|
||||||
if pct, err := sys.CPUPercentRaw(); err == nil {
|
s.mu.Lock()
|
||||||
s.mu.Lock()
|
// First call to native method returns 0 (initializes baseline)
|
||||||
// Smooth with EMA
|
if !s.hasNativeCPUSample {
|
||||||
const alpha = 0.3
|
s.hasNativeCPUSample = true
|
||||||
if s.emaCPU == 0 {
|
|
||||||
s.emaCPU = pct
|
|
||||||
} else {
|
|
||||||
s.emaCPU = alpha*pct + (1-alpha)*s.emaCPU
|
|
||||||
}
|
|
||||||
val := s.emaCPU
|
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
return val, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
// If native call fails, fall back to gopsutil times
|
// Smooth with EMA
|
||||||
|
const alpha = 0.3
|
||||||
|
if s.emaCPU == 0 {
|
||||||
|
s.emaCPU = pct
|
||||||
|
} else {
|
||||||
|
s.emaCPU = alpha*pct + (1-alpha)*s.emaCPU
|
||||||
|
}
|
||||||
|
val := s.emaCPU
|
||||||
|
s.mu.Unlock()
|
||||||
|
return val, nil
|
||||||
}
|
}
|
||||||
|
// If native call fails, fall back to gopsutil times
|
||||||
// Read aggregate CPU times (all CPUs combined)
|
// Read aggregate CPU times (all CPUs combined)
|
||||||
times, err := cpu.Times(false)
|
times, err := cpu.Times(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -463,17 +476,16 @@ func (s *ServerService) sampleCPUUtilization() (float64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compute busy and total deltas
|
// Compute busy and total deltas
|
||||||
|
// Note: Guest and GuestNice times are already included in User and Nice respectively,
|
||||||
|
// so we exclude them to avoid double-counting (Linux kernel accounting)
|
||||||
idleDelta := cur.Idle - s.lastCPUTimes.Idle
|
idleDelta := cur.Idle - s.lastCPUTimes.Idle
|
||||||
// Sum of busy deltas (exclude Idle)
|
|
||||||
busyDelta := (cur.User - s.lastCPUTimes.User) +
|
busyDelta := (cur.User - s.lastCPUTimes.User) +
|
||||||
(cur.System - s.lastCPUTimes.System) +
|
(cur.System - s.lastCPUTimes.System) +
|
||||||
(cur.Nice - s.lastCPUTimes.Nice) +
|
(cur.Nice - s.lastCPUTimes.Nice) +
|
||||||
(cur.Iowait - s.lastCPUTimes.Iowait) +
|
(cur.Iowait - s.lastCPUTimes.Iowait) +
|
||||||
(cur.Irq - s.lastCPUTimes.Irq) +
|
(cur.Irq - s.lastCPUTimes.Irq) +
|
||||||
(cur.Softirq - s.lastCPUTimes.Softirq) +
|
(cur.Softirq - s.lastCPUTimes.Softirq) +
|
||||||
(cur.Steal - s.lastCPUTimes.Steal) +
|
(cur.Steal - s.lastCPUTimes.Steal)
|
||||||
(cur.Guest - s.lastCPUTimes.Guest) +
|
|
||||||
(cur.GuestNice - s.lastCPUTimes.GuestNice)
|
|
||||||
|
|
||||||
totalDelta := busyDelta + idleDelta
|
totalDelta := busyDelta + idleDelta
|
||||||
|
|
||||||
@@ -690,14 +702,39 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str
|
|||||||
var lines []string
|
var lines []string
|
||||||
|
|
||||||
if syslog == "true" {
|
if syslog == "true" {
|
||||||
cmdArgs := []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count, "-p", level}
|
// Check if running on Windows - journalctl is not available
|
||||||
// Run the command
|
if runtime.GOOS == "windows" {
|
||||||
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
return []string{"Syslog is not supported on Windows. Please use application logs instead by unchecking the 'Syslog' option."}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and sanitize count parameter
|
||||||
|
countInt, err := strconv.Atoi(count)
|
||||||
|
if err != nil || countInt < 1 || countInt > 10000 {
|
||||||
|
return []string{"Invalid count parameter - must be a number between 1 and 10000"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate level parameter - only allow valid syslog levels
|
||||||
|
validLevels := map[string]bool{
|
||||||
|
"0": true, "emerg": true,
|
||||||
|
"1": true, "alert": true,
|
||||||
|
"2": true, "crit": true,
|
||||||
|
"3": true, "err": true,
|
||||||
|
"4": true, "warning": true,
|
||||||
|
"5": true, "notice": true,
|
||||||
|
"6": true, "info": true,
|
||||||
|
"7": true, "debug": true,
|
||||||
|
}
|
||||||
|
if !validLevels[level] {
|
||||||
|
return []string{"Invalid level parameter - must be a valid syslog level"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use hardcoded command with validated parameters
|
||||||
|
cmd := exec.Command("journalctl", "-u", "x-ui", "--no-pager", "-n", strconv.Itoa(countInt), "-p", level)
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
cmd.Stdout = &out
|
cmd.Stdout = &out
|
||||||
err := cmd.Run()
|
err = cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{"Failed to run journalctl command!"}
|
return []string{"Failed to run journalctl command! Make sure systemd is available and x-ui service is registered."}
|
||||||
}
|
}
|
||||||
lines = strings.Split(out.String(), "\n")
|
lines = strings.Split(out.String(), "\n")
|
||||||
} else {
|
} else {
|
||||||
@@ -905,13 +942,26 @@ func (s *ServerService) ImportDB(file multipart.File) error {
|
|||||||
return common.NewErrorf("Error saving db: %v", err)
|
return common.NewErrorf("Error saving db: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we can init the db or not
|
// Close temp file before opening via sqlite
|
||||||
if err = database.InitDB(tempPath); err != nil {
|
if err = tempFile.Close(); err != nil {
|
||||||
return common.NewErrorf("Error checking db: %v", err)
|
return common.NewErrorf("Error closing temporary db file: %v", err)
|
||||||
|
}
|
||||||
|
tempFile = nil
|
||||||
|
|
||||||
|
// Validate integrity (no migrations / side effects)
|
||||||
|
if err = database.ValidateSQLiteDB(tempPath); err != nil {
|
||||||
|
return common.NewErrorf("Invalid or corrupt db file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop Xray
|
// Stop Xray (ignore error but log)
|
||||||
s.StopXrayService()
|
if errStop := s.StopXrayService(); errStop != nil {
|
||||||
|
logger.Warningf("Failed to stop Xray before DB import: %v", errStop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close existing DB to release file locks (especially on Windows)
|
||||||
|
if errClose := database.CloseDB(); errClose != nil {
|
||||||
|
logger.Warningf("Failed to close existing DB before replacement: %v", errClose)
|
||||||
|
}
|
||||||
|
|
||||||
// Backup the current database for fallback
|
// Backup the current database for fallback
|
||||||
fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
|
fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
|
||||||
@@ -946,7 +996,7 @@ func (s *ServerService) ImportDB(file multipart.File) error {
|
|||||||
return common.NewErrorf("Error moving db file: %v", err)
|
return common.NewErrorf("Error moving db file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate DB
|
// Open & migrate new DB
|
||||||
if err = database.InitDB(config.GetDBPath()); err != nil {
|
if err = database.InitDB(config.GetDBPath()); err != nil {
|
||||||
if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil {
|
if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil {
|
||||||
return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
|
return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
|
||||||
@@ -964,6 +1014,35 @@ func (s *ServerService) ImportDB(file multipart.File) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValidGeofileName validates that the filename is safe for geofile operations.
|
||||||
|
// It checks for path traversal attempts and ensures the filename contains only safe characters.
|
||||||
|
func (s *ServerService) IsValidGeofileName(filename string) bool {
|
||||||
|
if filename == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for path traversal attempts
|
||||||
|
if strings.Contains(filename, "..") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for path separators (both forward and backward slash)
|
||||||
|
if strings.ContainsAny(filename, `/\`) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for absolute path indicators
|
||||||
|
if filepath.IsAbs(filename) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional security: only allow alphanumeric, dots, underscores, and hyphens
|
||||||
|
// This is stricter than the general filename regex
|
||||||
|
validGeofilePattern := `^[a-zA-Z0-9._-]+\.dat$`
|
||||||
|
matched, _ := regexp.MatchString(validGeofilePattern, filename)
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ServerService) UpdateGeofile(fileName string) error {
|
func (s *ServerService) UpdateGeofile(fileName string) error {
|
||||||
files := []struct {
|
files := []struct {
|
||||||
URL string
|
URL string
|
||||||
@@ -977,6 +1056,25 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
|
|||||||
{"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite_RU.dat"},
|
{"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite_RU.dat"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strict allowlist check to avoid writing uncontrolled files
|
||||||
|
if fileName != "" {
|
||||||
|
// Use the centralized validation function
|
||||||
|
if !s.IsValidGeofileName(fileName) {
|
||||||
|
return common.NewErrorf("Invalid geofile name: contains unsafe path characters: %s", fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the filename matches exactly one from our allowlist
|
||||||
|
isAllowed := false
|
||||||
|
for _, file := range files {
|
||||||
|
if fileName == file.FileName {
|
||||||
|
isAllowed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isAllowed {
|
||||||
|
return common.NewErrorf("Invalid geofile name: %s not in allowlist", fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
downloadFile := func(url, destPath string) error {
|
downloadFile := func(url, destPath string) error {
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1002,14 +1100,17 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
|
|||||||
|
|
||||||
if fileName == "" {
|
if fileName == "" {
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
destPath := fmt.Sprintf("%s/%s", config.GetBinFolderPath(), file.FileName)
|
// Sanitize the filename from our allowlist as an extra precaution
|
||||||
|
destPath := filepath.Join(config.GetBinFolderPath(), filepath.Base(file.FileName))
|
||||||
|
|
||||||
if err := downloadFile(file.URL, destPath); err != nil {
|
if err := downloadFile(file.URL, destPath); err != nil {
|
||||||
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", file.FileName, err))
|
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", file.FileName, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
destPath := fmt.Sprintf("%s/%s", config.GetBinFolderPath(), fileName)
|
// Use filepath.Base to ensure we only get the filename component, no path traversal
|
||||||
|
safeName := filepath.Base(fileName)
|
||||||
|
destPath := filepath.Join(config.GetBinFolderPath(), safeName)
|
||||||
|
|
||||||
var fileURL string
|
var fileURL string
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
@@ -1021,10 +1122,10 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
|
|||||||
|
|
||||||
if fileURL == "" {
|
if fileURL == "" {
|
||||||
errorMessages = append(errorMessages, fmt.Sprintf("File '%s' not found in the list of Geofiles", fileName))
|
errorMessages = append(errorMessages, fmt.Sprintf("File '%s' not found in the list of Geofiles", fileName))
|
||||||
}
|
} else {
|
||||||
|
if err := downloadFile(fileURL, destPath); err != nil {
|
||||||
if err := downloadFile(fileURL, destPath); err != nil {
|
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", fileName, err))
|
||||||
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", fileName, err))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"github.com/mhsanaei/3x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"github.com/mhsanaei/3x-ui/util/random"
|
"github.com/mhsanaei/3x-ui/v2/util/random"
|
||||||
"github.com/mhsanaei/3x-ui/util/reflect_util"
|
"github.com/mhsanaei/3x-ui/v2/util/reflect_util"
|
||||||
"github.com/mhsanaei/3x-ui/web/entity"
|
"github.com/mhsanaei/3x-ui/v2/web/entity"
|
||||||
"github.com/mhsanaei/3x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed config.json
|
//go:embed config.json
|
||||||
@@ -33,7 +33,7 @@ var defaultValueMap = map[string]string{
|
|||||||
"secret": random.Seq(32),
|
"secret": random.Seq(32),
|
||||||
"webBasePath": "/",
|
"webBasePath": "/",
|
||||||
"sessionMaxAge": "360",
|
"sessionMaxAge": "360",
|
||||||
"pageSize": "50",
|
"pageSize": "25",
|
||||||
"expireDiff": "0",
|
"expireDiff": "0",
|
||||||
"trafficDiff": "0",
|
"trafficDiff": "0",
|
||||||
"remarkModel": "-ieo",
|
"remarkModel": "-ieo",
|
||||||
@@ -73,8 +73,31 @@ var defaultValueMap = map[string]string{
|
|||||||
"warp": "",
|
"warp": "",
|
||||||
"externalTrafficInformEnable": "false",
|
"externalTrafficInformEnable": "false",
|
||||||
"externalTrafficInformURI": "",
|
"externalTrafficInformURI": "",
|
||||||
|
// LDAP defaults
|
||||||
|
"ldapEnable": "false",
|
||||||
|
"ldapHost": "",
|
||||||
|
"ldapPort": "389",
|
||||||
|
"ldapUseTLS": "false",
|
||||||
|
"ldapBindDN": "",
|
||||||
|
"ldapPassword": "",
|
||||||
|
"ldapBaseDN": "",
|
||||||
|
"ldapUserFilter": "(objectClass=person)",
|
||||||
|
"ldapUserAttr": "mail",
|
||||||
|
"ldapVlessField": "vless_enabled",
|
||||||
|
"ldapSyncCron": "@every 1m",
|
||||||
|
"ldapFlagField": "",
|
||||||
|
"ldapTruthyValues": "true,1,yes,on",
|
||||||
|
"ldapInvertFlag": "false",
|
||||||
|
"ldapInboundTags": "",
|
||||||
|
"ldapAutoCreate": "false",
|
||||||
|
"ldapAutoDelete": "false",
|
||||||
|
"ldapDefaultTotalGB": "0",
|
||||||
|
"ldapDefaultExpiryDays": "0",
|
||||||
|
"ldapDefaultLimitIP": "0",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SettingService provides business logic for application settings management.
|
||||||
|
// It handles configuration storage, retrieval, and validation for all system settings.
|
||||||
type SettingService struct{}
|
type SettingService struct{}
|
||||||
|
|
||||||
func (s *SettingService) GetDefaultJsonConfig() (any, error) {
|
func (s *SettingService) GetDefaultJsonConfig() (any, error) {
|
||||||
@@ -540,6 +563,87 @@ func (s *SettingService) GetIpLimitEnable() (bool, error) {
|
|||||||
return (accessLogPath != "none" && accessLogPath != ""), nil
|
return (accessLogPath != "none" && accessLogPath != ""), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LDAP exported getters
|
||||||
|
func (s *SettingService) GetLdapEnable() (bool, error) {
|
||||||
|
return s.getBool("ldapEnable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapHost() (string, error) {
|
||||||
|
return s.getString("ldapHost")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapPort() (int, error) {
|
||||||
|
return s.getInt("ldapPort")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapUseTLS() (bool, error) {
|
||||||
|
return s.getBool("ldapUseTLS")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapBindDN() (string, error) {
|
||||||
|
return s.getString("ldapBindDN")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapPassword() (string, error) {
|
||||||
|
return s.getString("ldapPassword")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapBaseDN() (string, error) {
|
||||||
|
return s.getString("ldapBaseDN")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapUserFilter() (string, error) {
|
||||||
|
return s.getString("ldapUserFilter")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapUserAttr() (string, error) {
|
||||||
|
return s.getString("ldapUserAttr")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapVlessField() (string, error) {
|
||||||
|
return s.getString("ldapVlessField")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapSyncCron() (string, error) {
|
||||||
|
return s.getString("ldapSyncCron")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapFlagField() (string, error) {
|
||||||
|
return s.getString("ldapFlagField")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapTruthyValues() (string, error) {
|
||||||
|
return s.getString("ldapTruthyValues")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapInvertFlag() (bool, error) {
|
||||||
|
return s.getBool("ldapInvertFlag")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapInboundTags() (string, error) {
|
||||||
|
return s.getString("ldapInboundTags")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapAutoCreate() (bool, error) {
|
||||||
|
return s.getBool("ldapAutoCreate")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapAutoDelete() (bool, error) {
|
||||||
|
return s.getBool("ldapAutoDelete")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapDefaultTotalGB() (int, error) {
|
||||||
|
return s.getInt("ldapDefaultTotalGB")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapDefaultExpiryDays() (int, error) {
|
||||||
|
return s.getInt("ldapDefaultExpiryDays")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetLdapDefaultLimitIP() (int, error) {
|
||||||
|
return s.getInt("ldapDefaultLimitIP")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
||||||
if err := allSetting.CheckValid(); err != nil {
|
if err := allSetting.CheckValid(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -16,16 +16,17 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"github.com/mhsanaei/3x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"github.com/mhsanaei/3x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"github.com/mhsanaei/3x-ui/web/global"
|
"github.com/mhsanaei/3x-ui/v2/web/global"
|
||||||
"github.com/mhsanaei/3x-ui/web/locale"
|
"github.com/mhsanaei/3x-ui/v2/web/locale"
|
||||||
"github.com/mhsanaei/3x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
@@ -44,6 +45,23 @@ var (
|
|||||||
hostname string
|
hostname string
|
||||||
hashStorage *global.HashStorage
|
hashStorage *global.HashStorage
|
||||||
|
|
||||||
|
// Performance improvements
|
||||||
|
messageWorkerPool chan struct{} // Semaphore for limiting concurrent message processing
|
||||||
|
optimizedHTTPClient *http.Client // HTTP client with connection pooling and timeouts
|
||||||
|
|
||||||
|
// Simple cache for frequently accessed data
|
||||||
|
statusCache struct {
|
||||||
|
data *Status
|
||||||
|
timestamp time.Time
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
serverStatsCache struct {
|
||||||
|
data string
|
||||||
|
timestamp time.Time
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
// clients data to adding new client
|
// clients data to adding new client
|
||||||
receiver_inbound_ID int
|
receiver_inbound_ID int
|
||||||
client_Id string
|
client_Id string
|
||||||
@@ -65,14 +83,18 @@ var (
|
|||||||
|
|
||||||
var userStates = make(map[int64]string)
|
var userStates = make(map[int64]string)
|
||||||
|
|
||||||
|
// LoginStatus represents the result of a login attempt.
|
||||||
type LoginStatus byte
|
type LoginStatus byte
|
||||||
|
|
||||||
|
// Login status constants
|
||||||
const (
|
const (
|
||||||
LoginSuccess LoginStatus = 1
|
LoginSuccess LoginStatus = 1 // Login was successful
|
||||||
LoginFail LoginStatus = 0
|
LoginFail LoginStatus = 0 // Login failed
|
||||||
EmptyTelegramUserID = int64(0)
|
EmptyTelegramUserID = int64(0) // Default value for empty Telegram user ID
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Tgbot provides business logic for Telegram bot integration.
|
||||||
|
// It handles bot commands, user interactions, and status reporting via Telegram.
|
||||||
type Tgbot struct {
|
type Tgbot struct {
|
||||||
inboundService InboundService
|
inboundService InboundService
|
||||||
settingService SettingService
|
settingService SettingService
|
||||||
@@ -81,18 +103,62 @@ type Tgbot struct {
|
|||||||
lastStatus *Status
|
lastStatus *Status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewTgbot creates a new Tgbot instance.
|
||||||
func (t *Tgbot) NewTgbot() *Tgbot {
|
func (t *Tgbot) NewTgbot() *Tgbot {
|
||||||
return new(Tgbot)
|
return new(Tgbot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// I18nBot retrieves a localized message for the bot interface.
|
||||||
func (t *Tgbot) I18nBot(name string, params ...string) string {
|
func (t *Tgbot) I18nBot(name string, params ...string) string {
|
||||||
return locale.I18n(locale.Bot, name, params...)
|
return locale.I18n(locale.Bot, name, params...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHashStorage returns the hash storage instance for callback queries.
|
||||||
func (t *Tgbot) GetHashStorage() *global.HashStorage {
|
func (t *Tgbot) GetHashStorage() *global.HashStorage {
|
||||||
return hashStorage
|
return hashStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getCachedStatus returns cached server status if it's fresh enough (less than 5 seconds old)
|
||||||
|
func (t *Tgbot) getCachedStatus() (*Status, bool) {
|
||||||
|
statusCache.mutex.RLock()
|
||||||
|
defer statusCache.mutex.RUnlock()
|
||||||
|
|
||||||
|
if statusCache.data != nil && time.Since(statusCache.timestamp) < 5*time.Second {
|
||||||
|
return statusCache.data, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCachedStatus updates the status cache
|
||||||
|
func (t *Tgbot) setCachedStatus(status *Status) {
|
||||||
|
statusCache.mutex.Lock()
|
||||||
|
defer statusCache.mutex.Unlock()
|
||||||
|
|
||||||
|
statusCache.data = status
|
||||||
|
statusCache.timestamp = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCachedServerStats returns cached server stats if it's fresh enough (less than 10 seconds old)
|
||||||
|
func (t *Tgbot) getCachedServerStats() (string, bool) {
|
||||||
|
serverStatsCache.mutex.RLock()
|
||||||
|
defer serverStatsCache.mutex.RUnlock()
|
||||||
|
|
||||||
|
if serverStatsCache.data != "" && time.Since(serverStatsCache.timestamp) < 10*time.Second {
|
||||||
|
return serverStatsCache.data, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCachedServerStats updates the server stats cache
|
||||||
|
func (t *Tgbot) setCachedServerStats(stats string) {
|
||||||
|
serverStatsCache.mutex.Lock()
|
||||||
|
defer serverStatsCache.mutex.Unlock()
|
||||||
|
|
||||||
|
serverStatsCache.data = stats
|
||||||
|
serverStatsCache.timestamp = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initializes and starts the Telegram bot with the provided translation files.
|
||||||
func (t *Tgbot) Start(i18nFS embed.FS) error {
|
func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
// Initialize localizer
|
// Initialize localizer
|
||||||
err := locale.InitLocalizer(i18nFS, &t.settingService)
|
err := locale.InitLocalizer(i18nFS, &t.settingService)
|
||||||
@@ -103,6 +169,20 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
|||||||
// Initialize hash storage to store callback queries
|
// Initialize hash storage to store callback queries
|
||||||
hashStorage = global.NewHashStorage(20 * time.Minute)
|
hashStorage = global.NewHashStorage(20 * time.Minute)
|
||||||
|
|
||||||
|
// Initialize worker pool for concurrent message processing (max 10 concurrent handlers)
|
||||||
|
messageWorkerPool = make(chan struct{}, 10)
|
||||||
|
|
||||||
|
// Initialize optimized HTTP client with connection pooling
|
||||||
|
optimizedHTTPClient = &http.Client{
|
||||||
|
Timeout: 15 * time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
MaxIdleConnsPerHost: 10,
|
||||||
|
IdleConnTimeout: 30 * time.Second,
|
||||||
|
DisableKeepAlives: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
t.SetHostname()
|
t.SetHostname()
|
||||||
|
|
||||||
// Get Telegram bot token
|
// Get Telegram bot token
|
||||||
@@ -173,6 +253,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewBot creates a new Telegram bot instance with optional proxy and API server settings.
|
||||||
func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
|
func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
|
||||||
if proxyUrl == "" && apiServerUrl == "" {
|
if proxyUrl == "" && apiServerUrl == "" {
|
||||||
return telego.NewBot(token)
|
return telego.NewBot(token)
|
||||||
@@ -209,10 +290,12 @@ func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*tel
|
|||||||
return telego.NewBot(token, telego.WithAPIServer(apiServerUrl))
|
return telego.NewBot(token, telego.WithAPIServer(apiServerUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsRunning checks if the Telegram bot is currently running.
|
||||||
func (t *Tgbot) IsRunning() bool {
|
func (t *Tgbot) IsRunning() bool {
|
||||||
return isRunning
|
return isRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetHostname sets the hostname for the bot.
|
||||||
func (t *Tgbot) SetHostname() {
|
func (t *Tgbot) SetHostname() {
|
||||||
host, err := os.Hostname()
|
host, err := os.Hostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -223,6 +306,7 @@ func (t *Tgbot) SetHostname() {
|
|||||||
hostname = host
|
hostname = host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop stops the Telegram bot and cleans up resources.
|
||||||
func (t *Tgbot) Stop() {
|
func (t *Tgbot) Stop() {
|
||||||
if botHandler != nil {
|
if botHandler != nil {
|
||||||
botHandler.Stop()
|
botHandler.Stop()
|
||||||
@@ -232,6 +316,7 @@ func (t *Tgbot) Stop() {
|
|||||||
adminIds = nil
|
adminIds = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encodeQuery encodes the query string if it's longer than 64 characters.
|
||||||
func (t *Tgbot) encodeQuery(query string) string {
|
func (t *Tgbot) encodeQuery(query string) string {
|
||||||
// NOTE: we only need to hash for more than 64 chars
|
// NOTE: we only need to hash for more than 64 chars
|
||||||
if len(query) <= 64 {
|
if len(query) <= 64 {
|
||||||
@@ -241,6 +326,7 @@ func (t *Tgbot) encodeQuery(query string) string {
|
|||||||
return hashStorage.SaveHash(query)
|
return hashStorage.SaveHash(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decodeQuery decodes a hashed query string back to its original form.
|
||||||
func (t *Tgbot) decodeQuery(query string) (string, error) {
|
func (t *Tgbot) decodeQuery(query string) (string, error) {
|
||||||
if !hashStorage.IsMD5(query) {
|
if !hashStorage.IsMD5(query) {
|
||||||
return query, nil
|
return query, nil
|
||||||
@@ -254,9 +340,10 @@ func (t *Tgbot) decodeQuery(query string) (string, error) {
|
|||||||
return decoded, nil
|
return decoded, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnReceive starts the message receiving loop for the Telegram bot.
|
||||||
func (t *Tgbot) OnReceive() {
|
func (t *Tgbot) OnReceive() {
|
||||||
params := telego.GetUpdatesParams{
|
params := telego.GetUpdatesParams{
|
||||||
Timeout: 10,
|
Timeout: 30, // Increased timeout to reduce API calls
|
||||||
}
|
}
|
||||||
|
|
||||||
updates, _ := bot.UpdatesViaLongPolling(context.Background(), ¶ms)
|
updates, _ := bot.UpdatesViaLongPolling(context.Background(), ¶ms)
|
||||||
@@ -270,14 +357,26 @@ func (t *Tgbot) OnReceive() {
|
|||||||
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
|
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
|
||||||
|
|
||||||
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||||
delete(userStates, message.Chat.ID)
|
// Use goroutine with worker pool for concurrent command processing
|
||||||
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
|
go func() {
|
||||||
|
messageWorkerPool <- struct{}{} // Acquire worker
|
||||||
|
defer func() { <-messageWorkerPool }() // Release worker
|
||||||
|
|
||||||
|
delete(userStates, message.Chat.ID)
|
||||||
|
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
|
||||||
|
}()
|
||||||
return nil
|
return nil
|
||||||
}, th.AnyCommand())
|
}, th.AnyCommand())
|
||||||
|
|
||||||
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
|
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
|
||||||
delete(userStates, query.Message.GetChat().ID)
|
// Use goroutine with worker pool for concurrent callback processing
|
||||||
t.answerCallback(&query, checkAdmin(query.From.ID))
|
go func() {
|
||||||
|
messageWorkerPool <- struct{}{} // Acquire worker
|
||||||
|
defer func() { <-messageWorkerPool }() // Release worker
|
||||||
|
|
||||||
|
delete(userStates, query.Message.GetChat().ID)
|
||||||
|
t.answerCallback(&query, checkAdmin(query.From.ID))
|
||||||
|
}()
|
||||||
return nil
|
return nil
|
||||||
}, th.AnyCallbackQueryWithMessage())
|
}, th.AnyCallbackQueryWithMessage())
|
||||||
|
|
||||||
@@ -430,6 +529,7 @@ func (t *Tgbot) OnReceive() {
|
|||||||
botHandler.Start()
|
botHandler.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// answerCommand processes incoming command messages from Telegram users.
|
||||||
func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) {
|
func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) {
|
||||||
msg, onlyMessage := "", false
|
msg, onlyMessage := "", false
|
||||||
|
|
||||||
@@ -505,7 +605,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to send the message based on onlyMessage flag.
|
// sendResponse sends the response message based on the onlyMessage flag.
|
||||||
func (t *Tgbot) sendResponse(chatId int64, msg string, onlyMessage, isAdmin bool) {
|
func (t *Tgbot) sendResponse(chatId int64, msg string, onlyMessage, isAdmin bool) {
|
||||||
if onlyMessage {
|
if onlyMessage {
|
||||||
t.SendMsgToTgbot(chatId, msg)
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
@@ -514,6 +614,7 @@ func (t *Tgbot) sendResponse(chatId int64, msg string, onlyMessage, isAdmin bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// randomLowerAndNum generates a random string of lowercase letters and numbers.
|
||||||
func (t *Tgbot) randomLowerAndNum(length int) string {
|
func (t *Tgbot) randomLowerAndNum(length int) string {
|
||||||
charset := "abcdefghijklmnopqrstuvwxyz0123456789"
|
charset := "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
bytes := make([]byte, length)
|
bytes := make([]byte, length)
|
||||||
@@ -524,6 +625,7 @@ func (t *Tgbot) randomLowerAndNum(length int) string {
|
|||||||
return string(bytes)
|
return string(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// randomShadowSocksPassword generates a random password for Shadowsocks.
|
||||||
func (t *Tgbot) randomShadowSocksPassword() string {
|
func (t *Tgbot) randomShadowSocksPassword() string {
|
||||||
array := make([]byte, 32)
|
array := make([]byte, 32)
|
||||||
_, err := rand.Read(array)
|
_, err := rand.Read(array)
|
||||||
@@ -533,6 +635,7 @@ func (t *Tgbot) randomShadowSocksPassword() string {
|
|||||||
return base64.StdEncoding.EncodeToString(array)
|
return base64.StdEncoding.EncodeToString(array)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// answerCallback processes callback queries from inline keyboards.
|
||||||
func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
|
func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
|
||||||
chatId := callbackQuery.Message.GetChat().ID
|
chatId := callbackQuery.Message.GetChat().ID
|
||||||
|
|
||||||
@@ -856,7 +959,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||||||
if len(dataArray) == 3 {
|
if len(dataArray) == 3 {
|
||||||
days, err := strconv.Atoi(dataArray[2])
|
days, err := strconv.Atoi(dataArray[2])
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var date int64 = 0
|
var date int64
|
||||||
if days > 0 {
|
if days > 0 {
|
||||||
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
|
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -960,7 +1063,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||||||
case "add_client_reset_exp_c":
|
case "add_client_reset_exp_c":
|
||||||
client_ExpiryTime = 0
|
client_ExpiryTime = 0
|
||||||
days, _ := strconv.Atoi(dataArray[1])
|
days, _ := strconv.Atoi(dataArray[1])
|
||||||
var date int64 = 0
|
var date int64
|
||||||
if client_ExpiryTime > 0 {
|
if client_ExpiryTime > 0 {
|
||||||
if client_ExpiryTime-time.Now().Unix()*1000 < 0 {
|
if client_ExpiryTime-time.Now().Unix()*1000 < 0 {
|
||||||
date = -int64(days * 24 * 60 * 60000)
|
date = -int64(days * 24 * 60 * 60000)
|
||||||
@@ -1815,6 +1918,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildInboundClientDataMessage builds a message with client data for the given inbound and protocol.
|
||||||
func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol model.Protocol) (string, error) {
|
func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol model.Protocol) (string, error) {
|
||||||
var message string
|
var message string
|
||||||
|
|
||||||
@@ -1864,6 +1968,7 @@ func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol mo
|
|||||||
return message, nil
|
return message, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildJSONForProtocol builds a JSON string for the given protocol with client data.
|
||||||
func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) {
|
func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) {
|
||||||
var jsonString string
|
var jsonString string
|
||||||
|
|
||||||
@@ -1942,6 +2047,7 @@ func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) {
|
|||||||
return jsonString, nil
|
return jsonString, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubmitAddClient submits the client addition request to the inbound service.
|
||||||
func (t *Tgbot) SubmitAddClient() (bool, error) {
|
func (t *Tgbot) SubmitAddClient() (bool, error) {
|
||||||
|
|
||||||
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||||
@@ -1964,6 +2070,7 @@ func (t *Tgbot) SubmitAddClient() (bool, error) {
|
|||||||
return t.inboundService.AddInboundClient(newInbound)
|
return t.inboundService.AddInboundClient(newInbound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkAdmin checks if the given Telegram ID is an admin.
|
||||||
func checkAdmin(tgId int64) bool {
|
func checkAdmin(tgId int64) bool {
|
||||||
for _, adminId := range adminIds {
|
for _, adminId := range adminIds {
|
||||||
if adminId == tgId {
|
if adminId == tgId {
|
||||||
@@ -1973,6 +2080,7 @@ func checkAdmin(tgId int64) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendAnswer sends a response message with an inline keyboard to the specified chat.
|
||||||
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
||||||
numericKeyboard := tu.InlineKeyboard(
|
numericKeyboard := tu.InlineKeyboard(
|
||||||
tu.InlineKeyboardRow(
|
tu.InlineKeyboardRow(
|
||||||
@@ -2028,6 +2136,7 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
|||||||
t.SendMsgToTgbot(chatId, msg, ReplyMarkup)
|
t.SendMsgToTgbot(chatId, msg, ReplyMarkup)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendMsgToTgbot sends a message to the Telegram bot with optional reply markup.
|
||||||
func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.ReplyMarkup) {
|
func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.ReplyMarkup) {
|
||||||
if !isRunning {
|
if !isRunning {
|
||||||
return
|
return
|
||||||
@@ -2074,7 +2183,10 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("Error sending telegram message :", err)
|
logger.Warning("Error sending telegram message :", err)
|
||||||
}
|
}
|
||||||
time.Sleep(500 * time.Millisecond)
|
// Reduced delay to improve performance (only needed for rate limiting)
|
||||||
|
if n < len(allMessages)-1 { // Only delay between messages, not after the last one
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2143,6 +2255,7 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
|
|||||||
return subURL, subJsonURL, nil
|
return subURL, subJsonURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendClientSubLinks sends the subscription links for the client to the chat.
|
||||||
func (t *Tgbot) sendClientSubLinks(chatId int64, email string) {
|
func (t *Tgbot) sendClientSubLinks(chatId int64, email string) {
|
||||||
subURL, subJsonURL, err := t.buildSubscriptionURLs(email)
|
subURL, subJsonURL, err := t.buildSubscriptionURLs(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2182,12 +2295,12 @@ func (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) {
|
|||||||
// Force plain text to avoid HTML page; controller respects Accept header
|
// Force plain text to avoid HTML page; controller respects Accept header
|
||||||
req.Header.Set("Accept", "text/plain, */*;q=0.1")
|
req.Header.Set("Accept", "text/plain, */*;q=0.1")
|
||||||
|
|
||||||
// Use default client with reasonable timeout via context
|
// Use optimized client with connection pooling
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := optimizedHTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
return
|
return
|
||||||
@@ -2297,7 +2410,7 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
if resp, err := http.DefaultClient.Do(req); err == nil {
|
if resp, err := optimizedHTTPClient.Do(req); err == nil {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
encoded, _ := t.settingService.GetSubEncrypt()
|
encoded, _ := t.settingService.GetSubEncrypt()
|
||||||
@@ -2330,7 +2443,10 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
|
|||||||
tu.FileFromBytes(png, filename),
|
tu.FileFromBytes(png, filename),
|
||||||
)
|
)
|
||||||
_, _ = bot.SendDocument(context.Background(), document)
|
_, _ = bot.SendDocument(context.Background(), document)
|
||||||
time.Sleep(200 * time.Millisecond)
|
// Reduced delay for better performance
|
||||||
|
if i < max-1 { // Only delay between documents, not after the last one
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2338,6 +2454,7 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendMsgToTgbotAdmins sends a message to all admin Telegram chats.
|
||||||
func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) {
|
func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) {
|
||||||
if len(replyMarkup) > 0 {
|
if len(replyMarkup) > 0 {
|
||||||
for _, adminId := range adminIds {
|
for _, adminId := range adminIds {
|
||||||
@@ -2350,6 +2467,7 @@ func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMark
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendReport sends a periodic report to admin chats.
|
||||||
func (t *Tgbot) SendReport() {
|
func (t *Tgbot) SendReport() {
|
||||||
runTime, err := t.settingService.GetTgbotRuntime()
|
runTime, err := t.settingService.GetTgbotRuntime()
|
||||||
if err == nil && len(runTime) > 0 {
|
if err == nil && len(runTime) > 0 {
|
||||||
@@ -2371,6 +2489,7 @@ func (t *Tgbot) SendReport() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendBackupToAdmins sends a database backup to admin chats.
|
||||||
func (t *Tgbot) SendBackupToAdmins() {
|
func (t *Tgbot) SendBackupToAdmins() {
|
||||||
if !t.IsRunning() {
|
if !t.IsRunning() {
|
||||||
return
|
return
|
||||||
@@ -2380,6 +2499,7 @@ func (t *Tgbot) SendBackupToAdmins() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendExhaustedToAdmins sends notifications about exhausted clients to admins.
|
||||||
func (t *Tgbot) sendExhaustedToAdmins() {
|
func (t *Tgbot) sendExhaustedToAdmins() {
|
||||||
if !t.IsRunning() {
|
if !t.IsRunning() {
|
||||||
return
|
return
|
||||||
@@ -2389,6 +2509,7 @@ func (t *Tgbot) sendExhaustedToAdmins() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getServerUsage retrieves and formats server usage information.
|
||||||
func (t *Tgbot) getServerUsage(chatId int64, messageID ...int) string {
|
func (t *Tgbot) getServerUsage(chatId int64, messageID ...int) string {
|
||||||
info := t.prepareServerUsageInfo()
|
info := t.prepareServerUsageInfo()
|
||||||
|
|
||||||
@@ -2410,11 +2531,22 @@ func (t *Tgbot) sendServerUsage() string {
|
|||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareServerUsageInfo prepares the server usage information string.
|
||||||
func (t *Tgbot) prepareServerUsageInfo() string {
|
func (t *Tgbot) prepareServerUsageInfo() string {
|
||||||
|
// Check if we have cached data first
|
||||||
|
if cachedStats, found := t.getCachedServerStats(); found {
|
||||||
|
return cachedStats
|
||||||
|
}
|
||||||
|
|
||||||
info, ipv4, ipv6 := "", "", ""
|
info, ipv4, ipv6 := "", "", ""
|
||||||
|
|
||||||
// get latest status of server
|
// get latest status of server with caching
|
||||||
t.lastStatus = t.serverService.GetStatus(t.lastStatus)
|
if cachedStatus, found := t.getCachedStatus(); found {
|
||||||
|
t.lastStatus = cachedStatus
|
||||||
|
} else {
|
||||||
|
t.lastStatus = t.serverService.GetStatus(t.lastStatus)
|
||||||
|
t.setCachedStatus(t.lastStatus)
|
||||||
|
}
|
||||||
onlines := p.GetOnlineClients()
|
onlines := p.GetOnlineClients()
|
||||||
|
|
||||||
info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
|
info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
|
||||||
@@ -2456,9 +2588,14 @@ func (t *Tgbot) prepareServerUsageInfo() string {
|
|||||||
info += t.I18nBot("tgbot.messages.udpCount", "Count=="+strconv.Itoa(t.lastStatus.UdpCount))
|
info += t.I18nBot("tgbot.messages.udpCount", "Count=="+strconv.Itoa(t.lastStatus.UdpCount))
|
||||||
info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), "Upload=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), "Download=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv)))
|
info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), "Upload=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), "Download=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv)))
|
||||||
info += t.I18nBot("tgbot.messages.xrayStatus", "State=="+fmt.Sprint(t.lastStatus.Xray.State))
|
info += t.I18nBot("tgbot.messages.xrayStatus", "State=="+fmt.Sprint(t.lastStatus.Xray.State))
|
||||||
|
|
||||||
|
// Cache the complete server stats
|
||||||
|
t.setCachedServerStats(info)
|
||||||
|
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserLoginNotify sends a notification about user login attempts to admins.
|
||||||
func (t *Tgbot) UserLoginNotify(username string, password string, ip string, time string, status LoginStatus) {
|
func (t *Tgbot) UserLoginNotify(username string, password string, ip string, time string, status LoginStatus) {
|
||||||
if !t.IsRunning() {
|
if !t.IsRunning() {
|
||||||
return
|
return
|
||||||
@@ -2490,6 +2627,7 @@ func (t *Tgbot) UserLoginNotify(username string, password string, ip string, tim
|
|||||||
t.SendMsgToTgbotAdmins(msg)
|
t.SendMsgToTgbotAdmins(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getInboundUsages retrieves and formats inbound usage information.
|
||||||
func (t *Tgbot) getInboundUsages() string {
|
func (t *Tgbot) getInboundUsages() string {
|
||||||
info := ""
|
info := ""
|
||||||
// get traffic
|
// get traffic
|
||||||
@@ -2515,6 +2653,8 @@ func (t *Tgbot) getInboundUsages() string {
|
|||||||
}
|
}
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getInbounds creates an inline keyboard with all inbounds.
|
||||||
func (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) {
|
func (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) {
|
||||||
inbounds, err := t.inboundService.GetAllInbounds()
|
inbounds, err := t.inboundService.GetAllInbounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2546,8 +2686,7 @@ func (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) {
|
|||||||
return keyboard, nil
|
return keyboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getInboundsFor builds an inline keyboard of inbounds where each button leads to a custom next action
|
// getInboundsFor builds an inline keyboard of inbounds for a custom next action.
|
||||||
// nextAction should be one of: get_clients_for_sub|get_clients_for_individual|get_clients_for_qr
|
|
||||||
func (t *Tgbot) getInboundsFor(nextAction string) (*telego.InlineKeyboardMarkup, error) {
|
func (t *Tgbot) getInboundsFor(nextAction string) (*telego.InlineKeyboardMarkup, error) {
|
||||||
inbounds, err := t.inboundService.GetAllInbounds()
|
inbounds, err := t.inboundService.GetAllInbounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2614,6 +2753,7 @@ func (t *Tgbot) getInboundClientsFor(inboundID int, action string) (*telego.Inli
|
|||||||
return keyboard, nil
|
return keyboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getInboundsAddClient creates an inline keyboard for adding clients to inbounds.
|
||||||
func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
|
func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
|
||||||
inbounds, err := t.inboundService.GetAllInbounds()
|
inbounds, err := t.inboundService.GetAllInbounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2656,6 +2796,7 @@ func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
|
|||||||
return keyboard, nil
|
return keyboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getInboundClients creates an inline keyboard with clients of a specific inbound.
|
||||||
func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) {
|
func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) {
|
||||||
inbound, err := t.inboundService.GetInbound(id)
|
inbound, err := t.inboundService.GetInbound(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2690,6 +2831,7 @@ func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error)
|
|||||||
return keyboard, nil
|
return keyboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clientInfoMsg formats client information message based on traffic and flags.
|
||||||
func (t *Tgbot) clientInfoMsg(
|
func (t *Tgbot) clientInfoMsg(
|
||||||
traffic *xray.ClientTraffic,
|
traffic *xray.ClientTraffic,
|
||||||
printEnabled bool,
|
printEnabled bool,
|
||||||
@@ -2796,6 +2938,7 @@ func (t *Tgbot) clientInfoMsg(
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getClientUsage retrieves and sends client usage information to the chat.
|
||||||
func (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) {
|
func (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) {
|
||||||
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
|
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2838,6 +2981,7 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) {
|
|||||||
t.SendAnswer(chatId, output, false)
|
t.SendAnswer(chatId, output, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// searchClientIps searches and sends client IP addresses for the given email.
|
||||||
func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
|
func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
|
||||||
ips, err := t.inboundService.GetInboundClientIps(email)
|
ips, err := t.inboundService.GetInboundClientIps(email)
|
||||||
if err != nil || len(ips) == 0 {
|
if err != nil || len(ips) == 0 {
|
||||||
@@ -2865,6 +3009,7 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clientTelegramUserInfo retrieves and sends Telegram user info for the client.
|
||||||
func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...int) {
|
func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...int) {
|
||||||
traffic, client, err := t.inboundService.GetClientByEmail(email)
|
traffic, client, err := t.inboundService.GetClientByEmail(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2917,6 +3062,7 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// searchClient searches for a client by email and sends the information.
|
||||||
func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
|
func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
|
||||||
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
|
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2962,6 +3108,7 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addClient handles the process of adding a new client to an inbound.
|
||||||
func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
|
func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
|
||||||
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3058,6 +3205,7 @@ func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// searchInbound searches for inbounds by remark and sends the results.
|
||||||
func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
||||||
inbounds, err := t.inboundService.SearchInbounds(remark)
|
inbounds, err := t.inboundService.SearchInbounds(remark)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3095,6 +3243,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getExhausted retrieves and sends information about exhausted clients.
|
||||||
func (t *Tgbot) getExhausted(chatId int64) {
|
func (t *Tgbot) getExhausted(chatId int64) {
|
||||||
trDiff := int64(0)
|
trDiff := int64(0)
|
||||||
exDiff := int64(0)
|
exDiff := int64(0)
|
||||||
@@ -3191,6 +3340,7 @@ func (t *Tgbot) getExhausted(chatId int64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// notifyExhausted sends notifications for exhausted clients.
|
||||||
func (t *Tgbot) notifyExhausted() {
|
func (t *Tgbot) notifyExhausted() {
|
||||||
trDiff := int64(0)
|
trDiff := int64(0)
|
||||||
exDiff := int64(0)
|
exDiff := int64(0)
|
||||||
@@ -3262,6 +3412,7 @@ func (t *Tgbot) notifyExhausted() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// int64Contains checks if an int64 slice contains a specific item.
|
||||||
func int64Contains(slice []int64, item int64) bool {
|
func int64Contains(slice []int64, item int64) bool {
|
||||||
for _, s := range slice {
|
for _, s := range slice {
|
||||||
if s == item {
|
if s == item {
|
||||||
@@ -3271,6 +3422,7 @@ func int64Contains(slice []int64, item int64) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// onlineClients retrieves and sends information about online clients.
|
||||||
func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
|
func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
|
||||||
if !p.IsRunning() {
|
if !p.IsRunning() {
|
||||||
return
|
return
|
||||||
@@ -3305,6 +3457,7 @@ func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendBackup sends a backup of the database and configuration files.
|
||||||
func (t *Tgbot) sendBackup(chatId int64) {
|
func (t *Tgbot) sendBackup(chatId int64) {
|
||||||
output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||||
t.SendMsgToTgbot(chatId, output)
|
t.SendMsgToTgbot(chatId, output)
|
||||||
@@ -3344,6 +3497,7 @@ func (t *Tgbot) sendBackup(chatId int64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendBanLogs sends the ban logs to the specified chat.
|
||||||
func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
|
func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
|
||||||
if dt {
|
if dt {
|
||||||
output := t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05"))
|
output := t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||||
@@ -3393,6 +3547,7 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendCallbackAnswerTgBot answers a callback query with a message.
|
||||||
func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
|
func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
|
||||||
params := telego.AnswerCallbackQueryParams{
|
params := telego.AnswerCallbackQueryParams{
|
||||||
CallbackQueryID: id,
|
CallbackQueryID: id,
|
||||||
@@ -3403,6 +3558,7 @@ func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editMessageCallbackTgBot edits the reply markup of a message.
|
||||||
func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard *telego.InlineKeyboardMarkup) {
|
func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard *telego.InlineKeyboardMarkup) {
|
||||||
params := telego.EditMessageReplyMarkupParams{
|
params := telego.EditMessageReplyMarkupParams{
|
||||||
ChatID: tu.ID(chatId),
|
ChatID: tu.ID(chatId),
|
||||||
@@ -3414,6 +3570,7 @@ func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editMessageTgBot edits the text and reply markup of a message.
|
||||||
func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlineKeyboard ...*telego.InlineKeyboardMarkup) {
|
func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlineKeyboard ...*telego.InlineKeyboardMarkup) {
|
||||||
params := telego.EditMessageTextParams{
|
params := telego.EditMessageTextParams{
|
||||||
ChatID: tu.ID(chatId),
|
ChatID: tu.ID(chatId),
|
||||||
@@ -3429,6 +3586,7 @@ func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendMsgToTgbotDeleteAfter sends a message and deletes it after a specified delay.
|
||||||
func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSeconds int, replyMarkup ...telego.ReplyMarkup) {
|
func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSeconds int, replyMarkup ...telego.ReplyMarkup) {
|
||||||
// Determine if replyMarkup was passed; otherwise, set it to nil
|
// Determine if replyMarkup was passed; otherwise, set it to nil
|
||||||
var replyMarkupParam telego.ReplyMarkup
|
var replyMarkupParam telego.ReplyMarkup
|
||||||
@@ -3455,6 +3613,7 @@ func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSecon
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deleteMessageTgBot deletes a message from the chat.
|
||||||
func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) {
|
func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) {
|
||||||
params := telego.DeleteMessageParams{
|
params := telego.DeleteMessageParams{
|
||||||
ChatID: tu.ID(chatId),
|
ChatID: tu.ID(chatId),
|
||||||
@@ -3467,6 +3626,7 @@ func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isSingleWord checks if the text contains only a single word.
|
||||||
func (t *Tgbot) isSingleWord(text string) bool {
|
func (t *Tgbot) isSingleWord(text string) bool {
|
||||||
text = strings.TrimSpace(text)
|
text = strings.TrimSpace(text)
|
||||||
re := regexp.MustCompile(`\s+`)
|
re := regexp.MustCompile(`\s+`)
|
||||||
|
|||||||
@@ -3,19 +3,23 @@ package service
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"github.com/mhsanaei/3x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/util/crypto"
|
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||||
|
ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
|
||||||
"github.com/xlzd/gotp"
|
"github.com/xlzd/gotp"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UserService provides business logic for user management and authentication.
|
||||||
|
// It handles user creation, login, password management, and 2FA operations.
|
||||||
type UserService struct {
|
type UserService struct {
|
||||||
settingService SettingService
|
settingService SettingService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFirstUser retrieves the first user from the database.
|
||||||
|
// This is typically used for initial setup or when there's only one admin user.
|
||||||
func (s *UserService) GetFirstUser() (*model.User, error) {
|
func (s *UserService) GetFirstUser() (*model.User, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
@@ -45,9 +49,38 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !crypto.CheckPasswordHash(user.Password, password) {
|
// If LDAP enabled and local password check fails, attempt LDAP auth
|
||||||
return nil
|
if !crypto.CheckPasswordHash(user.Password, password) {
|
||||||
}
|
ldapEnabled, _ := s.settingService.GetLdapEnable()
|
||||||
|
if !ldapEnabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _ := s.settingService.GetLdapHost()
|
||||||
|
port, _ := s.settingService.GetLdapPort()
|
||||||
|
useTLS, _ := s.settingService.GetLdapUseTLS()
|
||||||
|
bindDN, _ := s.settingService.GetLdapBindDN()
|
||||||
|
ldapPass, _ := s.settingService.GetLdapPassword()
|
||||||
|
baseDN, _ := s.settingService.GetLdapBaseDN()
|
||||||
|
userFilter, _ := s.settingService.GetLdapUserFilter()
|
||||||
|
userAttr, _ := s.settingService.GetLdapUserAttr()
|
||||||
|
|
||||||
|
cfg := ldaputil.Config{
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
UseTLS: useTLS,
|
||||||
|
BindDN: bindDN,
|
||||||
|
Password: ldapPass,
|
||||||
|
BaseDN: baseDN,
|
||||||
|
UserFilter: userFilter,
|
||||||
|
UserAttr: userAttr,
|
||||||
|
}
|
||||||
|
ok, err := ldaputil.AuthenticateUser(cfg, username, password)
|
||||||
|
if err != nil || !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// On successful LDAP auth, continue 2FA checks below
|
||||||
|
}
|
||||||
|
|
||||||
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
|
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WarpService provides business logic for Cloudflare WARP integration.
|
||||||
|
// It manages WARP configuration and connectivity settings.
|
||||||
type WarpService struct {
|
type WarpService struct {
|
||||||
SettingService
|
SettingService
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"go.uber.org/atomic"
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
@@ -20,16 +20,20 @@ var (
|
|||||||
result string
|
result string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// XrayService provides business logic for Xray process management.
|
||||||
|
// It handles starting, stopping, restarting Xray, and managing its configuration.
|
||||||
type XrayService struct {
|
type XrayService struct {
|
||||||
inboundService InboundService
|
inboundService InboundService
|
||||||
settingService SettingService
|
settingService SettingService
|
||||||
xrayAPI xray.XrayAPI
|
xrayAPI xray.XrayAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsXrayRunning checks if the Xray process is currently running.
|
||||||
func (s *XrayService) IsXrayRunning() bool {
|
func (s *XrayService) IsXrayRunning() bool {
|
||||||
return p != nil && p.IsRunning()
|
return p != nil && p.IsRunning()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetXrayErr returns the error from the Xray process, if any.
|
||||||
func (s *XrayService) GetXrayErr() error {
|
func (s *XrayService) GetXrayErr() error {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -46,6 +50,7 @@ func (s *XrayService) GetXrayErr() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetXrayResult returns the result string from the Xray process.
|
||||||
func (s *XrayService) GetXrayResult() string {
|
func (s *XrayService) GetXrayResult() string {
|
||||||
if result != "" {
|
if result != "" {
|
||||||
return result
|
return result
|
||||||
@@ -68,6 +73,7 @@ func (s *XrayService) GetXrayResult() string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetXrayVersion returns the version of the running Xray process.
|
||||||
func (s *XrayService) GetXrayVersion() string {
|
func (s *XrayService) GetXrayVersion() string {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
@@ -75,10 +81,13 @@ func (s *XrayService) GetXrayVersion() string {
|
|||||||
return p.GetVersion()
|
return p.GetVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveIndex removes an element at the specified index from a slice.
|
||||||
|
// Returns a new slice with the element removed.
|
||||||
func RemoveIndex(s []any, index int) []any {
|
func RemoveIndex(s []any, index int) []any {
|
||||||
return append(s[:index], s[index+1:]...)
|
return append(s[:index], s[index+1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetXrayConfig retrieves and builds the Xray configuration from settings and inbounds.
|
||||||
func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||||
templateConfig, err := s.settingService.GetXrayConfigTemplate()
|
templateConfig, err := s.settingService.GetXrayConfigTemplate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -182,6 +191,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
|||||||
return xrayConfig, nil
|
return xrayConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetXrayTraffic fetches the current traffic statistics from the running Xray process.
|
||||||
func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
|
func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
|
||||||
if !s.IsXrayRunning() {
|
if !s.IsXrayRunning() {
|
||||||
err := errors.New("xray is not running")
|
err := errors.New("xray is not running")
|
||||||
@@ -200,6 +210,7 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic,
|
|||||||
return traffic, clientTraffic, nil
|
return traffic, clientTraffic, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RestartXray restarts the Xray process, optionally forcing a restart even if config unchanged.
|
||||||
func (s *XrayService) RestartXray(isForce bool) error {
|
func (s *XrayService) RestartXray(isForce bool) error {
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
@@ -229,6 +240,7 @@ func (s *XrayService) RestartXray(isForce bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StopXray stops the running Xray process.
|
||||||
func (s *XrayService) StopXray() error {
|
func (s *XrayService) StopXray() error {
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
@@ -240,15 +252,17 @@ func (s *XrayService) StopXray() error {
|
|||||||
return errors.New("xray is not running")
|
return errors.New("xray is not running")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetToNeedRestart marks that Xray needs to be restarted.
|
||||||
func (s *XrayService) SetToNeedRestart() {
|
func (s *XrayService) SetToNeedRestart() {
|
||||||
isNeedXrayRestart.Store(true)
|
isNeedXrayRestart.Store(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsNeedRestartAndSetFalse checks if restart is needed and resets the flag to false.
|
||||||
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
|
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
|
||||||
return isNeedXrayRestart.CompareAndSwap(true, false)
|
return isNeedXrayRestart.CompareAndSwap(true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Xray is not running and wasn't stopped manually, i.e. crashed
|
// DidXrayCrash checks if Xray crashed by verifying it's not running and wasn't manually stopped.
|
||||||
func (s *XrayService) DidXrayCrash() bool {
|
func (s *XrayService) DidXrayCrash() bool {
|
||||||
return !s.IsXrayRunning() && !isManuallyStopped.Load()
|
return !s.IsXrayRunning() && !isManuallyStopped.Load()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"github.com/mhsanaei/3x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// XraySettingService provides business logic for Xray configuration management.
|
||||||
|
// It handles validation and storage of Xray template configurations.
|
||||||
type XraySettingService struct {
|
type XraySettingService struct {
|
||||||
SettingService
|
SettingService
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
// Package session provides session management utilities for the 3x-ui web panel.
|
||||||
|
// It handles user authentication state, login sessions, and session storage using Gin sessions.
|
||||||
package session
|
package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -19,6 +21,8 @@ func init() {
|
|||||||
gob.Register(model.User{})
|
gob.Register(model.User{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLoginUser stores the authenticated user in the session.
|
||||||
|
// The user object is serialized and stored for subsequent requests.
|
||||||
func SetLoginUser(c *gin.Context, user *model.User) {
|
func SetLoginUser(c *gin.Context, user *model.User) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return
|
return
|
||||||
@@ -27,6 +31,8 @@ func SetLoginUser(c *gin.Context, user *model.User) {
|
|||||||
s.Set(loginUserKey, *user)
|
s.Set(loginUserKey, *user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMaxAge configures the session cookie maximum age in seconds.
|
||||||
|
// This controls how long the session remains valid before requiring re-authentication.
|
||||||
func SetMaxAge(c *gin.Context, maxAge int) {
|
func SetMaxAge(c *gin.Context, maxAge int) {
|
||||||
s := sessions.Default(c)
|
s := sessions.Default(c)
|
||||||
s.Options(sessions.Options{
|
s.Options(sessions.Options{
|
||||||
@@ -37,6 +43,8 @@ func SetMaxAge(c *gin.Context, maxAge int) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLoginUser retrieves the authenticated user from the session.
|
||||||
|
// Returns nil if no user is logged in or if the session data is invalid.
|
||||||
func GetLoginUser(c *gin.Context) *model.User {
|
func GetLoginUser(c *gin.Context) *model.User {
|
||||||
s := sessions.Default(c)
|
s := sessions.Default(c)
|
||||||
obj := s.Get(loginUserKey)
|
obj := s.Get(loginUserKey)
|
||||||
@@ -52,10 +60,14 @@ func GetLoginUser(c *gin.Context) *model.User {
|
|||||||
return &user
|
return &user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsLogin checks if a user is currently authenticated in the session.
|
||||||
|
// Returns true if a valid user session exists, false otherwise.
|
||||||
func IsLogin(c *gin.Context) bool {
|
func IsLogin(c *gin.Context) bool {
|
||||||
return GetLoginUser(c) != nil
|
return GetLoginUser(c) != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearSession removes all session data and invalidates the session.
|
||||||
|
// This effectively logs out the user and clears any stored session information.
|
||||||
func ClearSession(c *gin.Context) {
|
func ClearSession(c *gin.Context) {
|
||||||
s := sessions.Default(c)
|
s := sessions.Default(c)
|
||||||
s.Clear()
|
s.Clear()
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
|
|
||||||
[pages.login]
|
[pages.login]
|
||||||
"hello" = "Привет!"
|
"hello" = "Привет!"
|
||||||
"title" = "Приветствие!"
|
"title" = "Добро пожаловать!"
|
||||||
"loginAgain" = "Сессия истекла. Войдите в систему снова"
|
"loginAgain" = "Сессия истекла. Войдите в систему снова"
|
||||||
|
|
||||||
[pages.login.toasts]
|
[pages.login.toasts]
|
||||||
@@ -648,7 +648,7 @@
|
|||||||
"ips" = "🔢 IP-адреса:\r\n{{ .IPs }}\r\n"
|
"ips" = "🔢 IP-адреса:\r\n{{ .IPs }}\r\n"
|
||||||
"serverUpTime" = "⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\r\n"
|
"serverUpTime" = "⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\r\n"
|
||||||
"serverLoad" = "📈 Нагрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
"serverLoad" = "📈 Нагрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||||
"serverMemory" = "📋 Диск сервера: {{ .Current }}/{{ .Total }}\r\n"
|
"serverMemory" = "📋 ОЗУ сервера: {{ .Current }}/{{ .Total }}\r\n"
|
||||||
"tcpCount" = "🔹 Количество TCP-соединений: {{ .Count }}\r\n"
|
"tcpCount" = "🔹 Количество TCP-соединений: {{ .Count }}\r\n"
|
||||||
"udpCount" = "🔸 Количество UDP-соединений: {{ .Count }}\r\n"
|
"udpCount" = "🔸 Количество UDP-соединений: {{ .Count }}\r\n"
|
||||||
"traffic" = "🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
"traffic" = "🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
||||||
|
|||||||
67
web/web.go
67
web/web.go
@@ -1,3 +1,5 @@
|
|||||||
|
// Package web provides the main web server implementation for the 3x-ui panel,
|
||||||
|
// including HTTP/HTTPS serving, routing, templates, and background job scheduling.
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -14,15 +16,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"github.com/mhsanaei/3x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"github.com/mhsanaei/3x-ui/web/controller"
|
"github.com/mhsanaei/3x-ui/v2/web/controller"
|
||||||
"github.com/mhsanaei/3x-ui/web/job"
|
"github.com/mhsanaei/3x-ui/v2/web/job"
|
||||||
"github.com/mhsanaei/3x-ui/web/locale"
|
"github.com/mhsanaei/3x-ui/v2/web/locale"
|
||||||
"github.com/mhsanaei/3x-ui/web/middleware"
|
"github.com/mhsanaei/3x-ui/v2/web/middleware"
|
||||||
"github.com/mhsanaei/3x-ui/web/network"
|
"github.com/mhsanaei/3x-ui/v2/web/network"
|
||||||
"github.com/mhsanaei/3x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
||||||
"github.com/gin-contrib/gzip"
|
"github.com/gin-contrib/gzip"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
@@ -78,23 +80,24 @@ func (f *wrapAssetsFileInfo) ModTime() time.Time {
|
|||||||
return startTime
|
return startTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose embedded resources for reuse by other servers (e.g., sub server)
|
// EmbeddedHTML returns the embedded HTML templates filesystem for reuse by other servers.
|
||||||
func EmbeddedHTML() embed.FS {
|
func EmbeddedHTML() embed.FS {
|
||||||
return htmlFS
|
return htmlFS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EmbeddedAssets returns the embedded assets filesystem for reuse by other servers.
|
||||||
func EmbeddedAssets() embed.FS {
|
func EmbeddedAssets() embed.FS {
|
||||||
return assetsFS
|
return assetsFS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server represents the main web server for the 3x-ui panel with controllers, services, and scheduled jobs.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
|
|
||||||
index *controller.IndexController
|
index *controller.IndexController
|
||||||
server *controller.ServerController
|
panel *controller.XUIController
|
||||||
panel *controller.XUIController
|
api *controller.APIController
|
||||||
api *controller.APIController
|
|
||||||
|
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
@@ -106,6 +109,7 @@ type Server struct {
|
|||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewServer creates a new web 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{
|
||||||
@@ -114,6 +118,8 @@ func NewServer() *Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getHtmlFiles walks the local `web/html` directory and returns a list of
|
||||||
|
// template file paths. Used only in debug/development mode.
|
||||||
func (s *Server) getHtmlFiles() ([]string, error) {
|
func (s *Server) getHtmlFiles() ([]string, error) {
|
||||||
files := make([]string, 0)
|
files := make([]string, 0)
|
||||||
dir, _ := os.Getwd()
|
dir, _ := os.Getwd()
|
||||||
@@ -133,6 +139,9 @@ func (s *Server) getHtmlFiles() ([]string, error) {
|
|||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getHtmlTemplate parses embedded HTML templates from the bundled `htmlFS`
|
||||||
|
// using the provided template function map and returns the resulting
|
||||||
|
// template set for production usage.
|
||||||
func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template, error) {
|
func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template, error) {
|
||||||
t := template.New("").Funcs(funcMap)
|
t := template.New("").Funcs(funcMap)
|
||||||
err := fs.WalkDir(htmlFS, "html", func(path string, d fs.DirEntry, err error) error {
|
err := fs.WalkDir(htmlFS, "html", func(path string, d fs.DirEntry, err error) error {
|
||||||
@@ -156,6 +165,8 @@ func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template,
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initRouter initializes Gin, registers middleware, templates, static
|
||||||
|
// assets, controllers and returns the configured engine.
|
||||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
if config.IsDebug() {
|
if config.IsDebug() {
|
||||||
gin.SetMode(gin.DebugMode)
|
gin.SetMode(gin.DebugMode)
|
||||||
@@ -252,13 +263,24 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
g := engine.Group(basePath)
|
g := engine.Group(basePath)
|
||||||
|
|
||||||
s.index = controller.NewIndexController(g)
|
s.index = controller.NewIndexController(g)
|
||||||
s.server = controller.NewServerController(g)
|
|
||||||
s.panel = controller.NewXUIController(g)
|
s.panel = controller.NewXUIController(g)
|
||||||
s.api = controller.NewAPIController(g)
|
s.api = controller.NewAPIController(g)
|
||||||
|
|
||||||
|
// Chrome DevTools endpoint for debugging web apps
|
||||||
|
engine.GET("/.well-known/appspecific/com.chrome.devtools.json", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add a catch-all route to handle undefined paths and return 404
|
||||||
|
engine.NoRoute(func(c *gin.Context) {
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
return engine, nil
|
return engine, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// startTask schedules background jobs (Xray checks, traffic jobs, cron
|
||||||
|
// jobs) which the panel relies on for periodic maintenance and monitoring.
|
||||||
func (s *Server) startTask() {
|
func (s *Server) startTask() {
|
||||||
err := s.xrayService.RestartXray(true)
|
err := s.xrayService.RestartXray(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -297,6 +319,17 @@ func (s *Server) startTask() {
|
|||||||
// Run once a month, midnight, first of month
|
// Run once a month, midnight, first of month
|
||||||
s.cron.AddJob("@monthly", job.NewPeriodicTrafficResetJob("monthly"))
|
s.cron.AddJob("@monthly", job.NewPeriodicTrafficResetJob("monthly"))
|
||||||
|
|
||||||
|
// LDAP sync scheduling
|
||||||
|
if ldapEnabled, _ := s.settingService.GetLdapEnable(); ldapEnabled {
|
||||||
|
runtime, err := s.settingService.GetLdapSyncCron()
|
||||||
|
if err != nil || runtime == "" {
|
||||||
|
runtime = "@every 1m"
|
||||||
|
}
|
||||||
|
j := job.NewLdapSyncJob()
|
||||||
|
// job has zero-value services with method receivers that read settings on demand
|
||||||
|
s.cron.AddJob(runtime, j)
|
||||||
|
}
|
||||||
|
|
||||||
// Make a traffic condition every day, 8:30
|
// Make a traffic condition every day, 8:30
|
||||||
var entry cron.EntryID
|
var entry cron.EntryID
|
||||||
isTgbotenabled, err := s.settingService.GetTgbotEnabled()
|
isTgbotenabled, err := s.settingService.GetTgbotEnabled()
|
||||||
@@ -326,6 +359,7 @@ func (s *Server) startTask() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start initializes and starts the web server with configured settings, routes, and background jobs.
|
||||||
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() {
|
||||||
@@ -404,6 +438,7 @@ func (s *Server) Start() (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop gracefully shuts down the web server, stops Xray, cron jobs, and Telegram bot.
|
||||||
func (s *Server) Stop() error {
|
func (s *Server) Stop() error {
|
||||||
s.cancel()
|
s.cancel()
|
||||||
s.xrayService.StopXray()
|
s.xrayService.StopXray()
|
||||||
@@ -424,10 +459,12 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCron returns the server's cron scheduler instance.
|
||||||
func (s *Server) GetCron() *cron.Cron {
|
func (s *Server) GetCron() *cron.Cron {
|
||||||
return s.cron
|
return s.cron
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user