mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-13 19:49:12 +00:00
add ldap support and portable database MySQL
This commit is contained in:
parent
172f2ddaa7
commit
51b5d10edf
148 changed files with 78773 additions and 7403 deletions
|
@ -27,7 +27,7 @@ case $1 in
|
|||
esac
|
||||
mkdir -p build/bin
|
||||
cd build/bin
|
||||
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.9.11/Xray-linux-${ARCH}.zip"
|
||||
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.8.3/Xray-linux-${ARCH}.zip"
|
||||
unzip "Xray-linux-${ARCH}.zip"
|
||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
||||
mv xray "xray-linux-${FNAME}"
|
||||
|
|
28
Dockerfile
28
Dockerfile
|
@ -1,7 +1,7 @@
|
|||
# ========================================================
|
||||
# Stage: Builder
|
||||
# ========================================================
|
||||
FROM golang:1.25-alpine AS builder
|
||||
FROM golang:1.24-alpine AS builder
|
||||
WORKDIR /app
|
||||
ARG TARGETARCH
|
||||
|
||||
|
@ -9,13 +9,14 @@ RUN apk --no-cache --update add \
|
|||
build-base \
|
||||
gcc \
|
||||
wget \
|
||||
unzip
|
||||
unzip \
|
||||
mariadb-connector-c-dev
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV CGO_ENABLED=1
|
||||
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
|
||||
RUN go build -ldflags "-w -s" -o build/x-ui main.go
|
||||
RUN go build -o build/x-ui main.go
|
||||
RUN ./DockerInit.sh "$TARGETARCH"
|
||||
|
||||
# ========================================================
|
||||
|
@ -29,12 +30,18 @@ RUN apk add --no-cache --update \
|
|||
ca-certificates \
|
||||
tzdata \
|
||||
fail2ban \
|
||||
bash
|
||||
bash \
|
||||
mariadb-connector-c
|
||||
|
||||
COPY --from=builder /app/build/ /app/
|
||||
COPY --from=builder /app/DockerEntrypoint.sh /app/
|
||||
COPY --from=builder /app/DockerEntrypoint.sh /app/DockerEntrypoint.sh
|
||||
COPY --from=builder /app/x-ui.sh /usr/bin/x-ui
|
||||
|
||||
# Verify files exist and set permissions
|
||||
RUN ls -la /app/
|
||||
RUN ls -la /app/DockerEntrypoint.sh
|
||||
RUN chmod +x /app/DockerEntrypoint.sh
|
||||
|
||||
|
||||
# Configure fail2ban
|
||||
RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \
|
||||
|
@ -49,7 +56,12 @@ RUN chmod +x \
|
|||
/usr/bin/x-ui
|
||||
|
||||
ENV XUI_ENABLE_FAIL2BAN="true"
|
||||
EXPOSE 2053
|
||||
VOLUME [ "/etc/x-ui" ]
|
||||
CMD [ "./x-ui" ]
|
||||
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]
|
||||
|
||||
# Create a simple entrypoint script
|
||||
RUN echo '#!/bin/sh' > /entrypoint.sh && \
|
||||
echo '[ "$XUI_ENABLE_FAIL2BAN" = "true" ] && fail2ban-client -x start' >> /entrypoint.sh && \
|
||||
echo 'exec /app/x-ui' >> /entrypoint.sh && \
|
||||
chmod +x /entrypoint.sh
|
||||
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
||||
|
|
|
@ -7,13 +7,11 @@
|
|||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
**3X-UI** — لوحة تحكم متقدمة مفتوحة المصدر تعتمد على الويب مصممة لإدارة خادم Xray-core. توفر واجهة سهلة الاستخدام لتكوين ومراقبة بروتوكولات VPN والوكيل المختلفة.
|
||||
|
||||
|
@ -43,13 +41,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
|||
|
||||
**إذا كان هذا المشروع مفيدًا لك، فقد ترغب في إعطائه**:star2:
|
||||
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## النجوم عبر الزمن
|
||||
|
||||
|
|
|
@ -7,13 +7,11 @@
|
|||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
**3X-UI** — panel de control avanzado basado en web de código abierto diseñado para gestionar el servidor Xray-core. Ofrece una interfaz fácil de usar para configurar y monitorear varios protocolos VPN y proxy.
|
||||
|
||||
|
@ -43,14 +41,15 @@ Para documentación completa, visita la [Wiki del proyecto](https://github.com/M
|
|||
|
||||
**Si este proyecto te es útil, puedes darle una**:star2:
|
||||
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## Estrellas a lo Largo del Tiempo
|
||||
|
||||
|
|
|
@ -7,13 +7,11 @@
|
|||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
**3X-UI** — یک پنل کنترل پیشرفته مبتنی بر وب با کد باز که برای مدیریت سرور Xray-core طراحی شده است. این پنل یک رابط کاربری آسان برای پیکربندی و نظارت بر پروتکلهای مختلف VPN و پراکسی ارائه میدهد.
|
||||
|
||||
|
@ -43,14 +41,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
|||
|
||||
**اگر این پروژه برای شما مفید است، میتوانید به آن یک**:star2: بدهید
|
||||
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## ستارهها در طول زمان
|
||||
|
||||
|
|
27
README.md
27
README.md
|
@ -7,13 +7,11 @@
|
|||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
**3X-UI** — advanced, open-source web-based control panel designed for managing Xray-core server. It offers a user-friendly interface for configuring and monitoring various VPN and proxy protocols.
|
||||
|
||||
|
@ -43,14 +41,15 @@ For full documentation, please visit the [project Wiki](https://github.com/MHSan
|
|||
|
||||
**If this project is helpful to you, you may wish to give it a**:star2:
|
||||
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## Stargazers over Time
|
||||
|
||||
|
|
|
@ -7,13 +7,11 @@
|
|||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
**3X-UI** — продвинутая панель управления с открытым исходным кодом на основе веб-интерфейса, разработанная для управления сервером Xray-core. Предоставляет удобный интерфейс для настройки и мониторинга различных VPN и прокси-протоколов.
|
||||
|
||||
|
@ -43,14 +41,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
|||
|
||||
**Если этот проект полезен для вас, вы можете поставить ему**:star2:
|
||||
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## Звезды с течением времени
|
||||
|
||||
|
|
|
@ -7,13 +7,11 @@
|
|||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||
[](#)
|
||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
**3X-UI** — 一个基于网页的高级开源控制面板,专为管理 Xray-core 服务器而设计。它提供了用户友好的界面,用于配置和监控各种 VPN 和代理协议。
|
||||
|
||||
|
@ -43,14 +41,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
|||
|
||||
**如果这个项目对您有帮助,您可以给它一个**:star2:
|
||||
|
||||
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||
</a>
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
</br>
|
||||
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||
</a>
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## 随时间变化的星标数
|
||||
|
||||
|
|
BIN
build/x-ui
Normal file
BIN
build/x-ui
Normal file
Binary file not shown.
103
config/config.go
103
config/config.go
|
@ -1,14 +1,9 @@
|
|||
// Package config provides configuration management utilities for the 3x-ui panel,
|
||||
// including version information, logging levels, database paths, and environment variable handling.
|
||||
package config
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -18,29 +13,24 @@ var version string
|
|||
//go:embed name
|
||||
var name string
|
||||
|
||||
// LogLevel represents the logging level for the application.
|
||||
type LogLevel string
|
||||
|
||||
// Logging level constants
|
||||
const (
|
||||
Debug LogLevel = "debug"
|
||||
Info LogLevel = "info"
|
||||
Notice LogLevel = "notice"
|
||||
Warning LogLevel = "warning"
|
||||
Error LogLevel = "error"
|
||||
Debug LogLevel = "debug"
|
||||
Info LogLevel = "info"
|
||||
Notice LogLevel = "notice"
|
||||
Warn LogLevel = "warn"
|
||||
Error LogLevel = "error"
|
||||
)
|
||||
|
||||
// GetVersion returns the version string of the 3x-ui application.
|
||||
func GetVersion() string {
|
||||
return strings.TrimSpace(version)
|
||||
}
|
||||
|
||||
// GetName returns the name of the 3x-ui application.
|
||||
func GetName() string {
|
||||
return strings.TrimSpace(name)
|
||||
}
|
||||
|
||||
// GetLogLevel returns the current logging level based on environment variables or defaults to Info.
|
||||
func GetLogLevel() LogLevel {
|
||||
if IsDebug() {
|
||||
return Debug
|
||||
|
@ -52,12 +42,10 @@ func GetLogLevel() LogLevel {
|
|||
return LogLevel(logLevel)
|
||||
}
|
||||
|
||||
// IsDebug returns true if debug mode is enabled via the XUI_DEBUG environment variable.
|
||||
func IsDebug() bool {
|
||||
return os.Getenv("XUI_DEBUG") == "true"
|
||||
}
|
||||
|
||||
// GetBinFolderPath returns the path to the binary folder, defaulting to "bin" if not set via XUI_BIN_FOLDER.
|
||||
func GetBinFolderPath() string {
|
||||
binFolderPath := os.Getenv("XUI_BIN_FOLDER")
|
||||
if binFolderPath == "" {
|
||||
|
@ -66,91 +54,22 @@ func GetBinFolderPath() string {
|
|||
return binFolderPath
|
||||
}
|
||||
|
||||
func getBaseDir() string {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
exeDir := filepath.Dir(exePath)
|
||||
exeDirLower := strings.ToLower(filepath.ToSlash(exeDir))
|
||||
if strings.Contains(exeDirLower, "/appdata/local/temp/") || strings.Contains(exeDirLower, "/go-build") {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
return wd
|
||||
}
|
||||
return exeDir
|
||||
}
|
||||
|
||||
// GetDBFolderPath returns the path to the database folder based on environment variables or platform defaults.
|
||||
func GetDBFolderPath() string {
|
||||
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
|
||||
if dbFolderPath != "" {
|
||||
return dbFolderPath
|
||||
if dbFolderPath == "" {
|
||||
dbFolderPath = "/etc/x-ui"
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
return getBaseDir()
|
||||
}
|
||||
return "/etc/x-ui"
|
||||
return dbFolderPath
|
||||
}
|
||||
|
||||
// GetDBPath returns the full path to the database file.
|
||||
func GetDBPath() string {
|
||||
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
||||
}
|
||||
|
||||
// GetLogFolder returns the path to the log folder based on environment variables or platform defaults.
|
||||
func GetLogFolder() string {
|
||||
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
|
||||
if logFolderPath != "" {
|
||||
return logFolderPath
|
||||
if logFolderPath == "" {
|
||||
logFolderPath = "/var/log"
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(".", "log")
|
||||
}
|
||||
return "/var/log"
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return out.Sync()
|
||||
}
|
||||
|
||||
func init() {
|
||||
if runtime.GOOS != "windows" {
|
||||
return
|
||||
}
|
||||
if os.Getenv("XUI_DB_FOLDER") != "" {
|
||||
return
|
||||
}
|
||||
oldDBFolder := "/etc/x-ui"
|
||||
oldDBPath := fmt.Sprintf("%s/%s.db", oldDBFolder, GetName())
|
||||
newDBFolder := GetDBFolderPath()
|
||||
newDBPath := fmt.Sprintf("%s/%s.db", newDBFolder, GetName())
|
||||
_, err := os.Stat(newDBPath)
|
||||
if err == nil {
|
||||
return // new exists
|
||||
}
|
||||
_, err = os.Stat(oldDBPath)
|
||||
if os.IsNotExist(err) {
|
||||
return // old does not exist
|
||||
}
|
||||
_ = copyFile(oldDBPath, newDBPath) // ignore error
|
||||
return logFolderPath
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.8.4
|
||||
2.6.6
|
|
@ -1,22 +1,18 @@
|
|||
// Package database provides database initialization, migration, and management utilities
|
||||
// for the 3x-ui panel using GORM with SQLite.
|
||||
package database
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||
"x-ui/config"
|
||||
"x-ui/database/model"
|
||||
"x-ui/util/crypto"
|
||||
"x-ui/xray"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
@ -47,7 +43,6 @@ func initModels() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// initUser creates a default admin user if the users table is empty.
|
||||
func initUser() error {
|
||||
empty, err := isTableEmpty("users")
|
||||
if err != nil {
|
||||
|
@ -71,7 +66,15 @@ func initUser() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// runSeeders migrates user passwords to bcrypt and records seeder execution to prevent re-running.
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return s == item
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func runSeeders(isUsersEmpty bool) error {
|
||||
empty, err := isTableEmpty("history_of_seeders")
|
||||
if err != nil {
|
||||
|
@ -88,7 +91,7 @@ func runSeeders(isUsersEmpty bool) error {
|
|||
var seedersHistory []string
|
||||
db.Model(&model.HistoryOfSeeders{}).Pluck("seeder_name", &seedersHistory)
|
||||
|
||||
if !slices.Contains(seedersHistory, "UserPasswordHash") && !isUsersEmpty {
|
||||
if !contains(seedersHistory, "UserPasswordHash") && !isUsersEmpty {
|
||||
var users []model.User
|
||||
db.Find(&users)
|
||||
|
||||
|
@ -111,21 +114,13 @@ func runSeeders(isUsersEmpty bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// isTableEmpty returns true if the named table contains zero rows.
|
||||
func isTableEmpty(tableName string) (bool, error) {
|
||||
var count int64
|
||||
err := db.Table(tableName).Count(&count).Error
|
||||
return count == 0, err
|
||||
}
|
||||
|
||||
// InitDB sets up the database connection, migrates models, and runs seeders.
|
||||
func InitDB(dbPath string) error {
|
||||
dir := path.Dir(dbPath)
|
||||
err := os.MkdirAll(dir, fs.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var gormLogger logger.Interface
|
||||
|
||||
if config.IsDebug() {
|
||||
|
@ -134,12 +129,25 @@ func InitDB(dbPath string) error {
|
|||
gormLogger = logger.Discard
|
||||
}
|
||||
|
||||
c := &gorm.Config{
|
||||
Logger: gormLogger,
|
||||
dbHost := os.Getenv("DB_HOST")
|
||||
dbPort := os.Getenv("DB_PORT")
|
||||
dbName := os.Getenv("DB_NAME")
|
||||
dbUser := os.Getenv("DB_USER")
|
||||
dbPass := os.Getenv("DB_PASSWORD")
|
||||
|
||||
if dbHost == "" || dbPort == "" || dbName == "" || dbUser == "" || dbPass == "" {
|
||||
return fmt.Errorf("missing database configuration environment variables")
|
||||
}
|
||||
db, err = gorm.Open(sqlite.Open(dbPath), c)
|
||||
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
dbUser, dbPass, dbHost, dbPort, dbName)
|
||||
|
||||
var err error
|
||||
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
Logger: gormLogger,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
if err := initModels(); err != nil {
|
||||
|
@ -154,10 +162,10 @@ func InitDB(dbPath string) error {
|
|||
if err := initUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runSeeders(isUsersEmpty)
|
||||
}
|
||||
|
||||
// CloseDB closes the database connection if it exists.
|
||||
func CloseDB() error {
|
||||
if db != nil {
|
||||
sqlDB, err := db.DB()
|
||||
|
@ -169,17 +177,14 @@ func CloseDB() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetDB returns the global GORM database instance.
|
||||
func GetDB() *gorm.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
// IsNotFound checks if the given error is a GORM record not found error.
|
||||
func IsNotFound(err error) bool {
|
||||
return err == gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
// IsSQLiteDB checks if the given file is a valid SQLite database by reading its signature.
|
||||
func IsSQLiteDB(file io.ReaderAt) (bool, error) {
|
||||
signature := []byte("SQLite format 3\x00")
|
||||
buf := make([]byte, len(signature))
|
||||
|
@ -190,12 +195,7 @@ func IsSQLiteDB(file io.ReaderAt) (bool, error) {
|
|||
return bytes.Equal(buf, signature), nil
|
||||
}
|
||||
|
||||
// Checkpoint performs a WAL checkpoint on the SQLite database to ensure data consistency.
|
||||
func Checkpoint() error {
|
||||
// Update WAL
|
||||
err := db.Exec("PRAGMA wal_checkpoint;").Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// MariaDB doesn't need WAL checkpoint
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,51 +1,43 @@
|
|||
// Package model defines the database models and data structures used by the 3x-ui panel.
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||
"x-ui/util/json_util"
|
||||
"x-ui/xray"
|
||||
)
|
||||
|
||||
// Protocol represents the protocol type for Xray inbounds.
|
||||
type Protocol string
|
||||
|
||||
// Protocol constants for different Xray inbound protocols
|
||||
const (
|
||||
VMESS Protocol = "vmess"
|
||||
VLESS Protocol = "vless"
|
||||
Tunnel Protocol = "tunnel"
|
||||
DOKODEMO Protocol = "dokodemo-door"
|
||||
HTTP Protocol = "http"
|
||||
Trojan Protocol = "trojan"
|
||||
Shadowsocks Protocol = "shadowsocks"
|
||||
Mixed Protocol = "mixed"
|
||||
Socks Protocol = "socks"
|
||||
WireGuard Protocol = "wireguard"
|
||||
)
|
||||
|
||||
// User represents a user account in the 3x-ui panel.
|
||||
type User struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// Inbound represents an Xray inbound configuration with traffic statistics and settings.
|
||||
type Inbound struct {
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier
|
||||
UserId int `json:"-"` // Associated user ID
|
||||
Up int64 `json:"up" form:"up"` // Upload traffic in bytes
|
||||
Down int64 `json:"down" form:"down"` // Download traffic in bytes
|
||||
Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
|
||||
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` // All-time traffic usage
|
||||
Remark string `json:"remark" form:"remark"` // Human-readable remark
|
||||
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
|
||||
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2"` // Traffic reset schedule
|
||||
LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
|
||||
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
UserId int `json:"-"`
|
||||
Up int64 `json:"up" form:"up"`
|
||||
Down int64 `json:"down" form:"down"`
|
||||
Total int64 `json:"total" form:"total"`
|
||||
Remark string `json:"remark" form:"remark"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`
|
||||
|
||||
// Xray configuration fields
|
||||
// config part
|
||||
Listen string `json:"listen" form:"listen"`
|
||||
Port int `json:"port" form:"port"`
|
||||
Protocol Protocol `json:"protocol" form:"protocol"`
|
||||
|
@ -53,9 +45,9 @@ type Inbound struct {
|
|||
StreamSettings string `json:"streamSettings" form:"streamSettings"`
|
||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||
Sniffing string `json:"sniffing" form:"sniffing"`
|
||||
Allocate string `json:"allocate" form:"allocate"`
|
||||
}
|
||||
|
||||
// OutboundTraffics tracks traffic statistics for Xray outbound connections.
|
||||
type OutboundTraffics struct {
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||
|
@ -64,20 +56,17 @@ type OutboundTraffics struct {
|
|||
Total int64 `json:"total" form:"total" gorm:"default:0"`
|
||||
}
|
||||
|
||||
// InboundClientIps stores IP addresses associated with inbound clients for access control.
|
||||
type InboundClientIps struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
|
||||
Ips string `json:"ips" form:"ips"`
|
||||
}
|
||||
|
||||
// HistoryOfSeeders tracks which database seeders have been executed to prevent re-running.
|
||||
type HistoryOfSeeders struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
SeederName string `json:"seederName"`
|
||||
}
|
||||
|
||||
// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
|
||||
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||
listen := i.Listen
|
||||
if listen != "" {
|
||||
|
@ -91,31 +80,30 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
|||
StreamSettings: json_util.RawMessage(i.StreamSettings),
|
||||
Tag: i.Tag,
|
||||
Sniffing: json_util.RawMessage(i.Sniffing),
|
||||
Allocate: json_util.RawMessage(i.Allocate),
|
||||
}
|
||||
}
|
||||
|
||||
// Setting stores key-value configuration settings for the 3x-ui panel.
|
||||
type Setting struct {
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Key string `json:"key" form:"key"`
|
||||
Value string `json:"value" form:"value"`
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Key string `gorm:"column:key"`
|
||||
Value string
|
||||
}
|
||||
|
||||
// Client represents a client configuration for Xray inbounds with traffic limits and settings.
|
||||
|
||||
|
||||
type Client struct {
|
||||
ID string `json:"id"` // Unique client identifier
|
||||
Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm")
|
||||
Password string `json:"password"` // Client password
|
||||
Flow string `json:"flow"` // Flow control (XTLS)
|
||||
Email string `json:"email"` // Client email identifier
|
||||
LimitIP int `json:"limitIp"` // IP limit for this client
|
||||
TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
|
||||
Enable bool `json:"enable" form:"enable"` // Whether the client is enabled
|
||||
TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications
|
||||
SubID string `json:"subId" form:"subId"` // Subscription identifier
|
||||
Comment string `json:"comment" form:"comment"` // Client comment
|
||||
Reset int `json:"reset" form:"reset"` // Reset period in days
|
||||
CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
|
||||
UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
|
||||
ID string `json:"id"`
|
||||
Security string `json:"security"`
|
||||
Password string `json:"password"`
|
||||
Flow string `json:"flow"`
|
||||
Email string `json:"email"`
|
||||
LimitIP int `json:"limitIp"`
|
||||
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
TgID int64 `json:"tgId" form:"tgId"`
|
||||
SubID string `json:"subId" form:"subId"`
|
||||
Comment string `json:"comment" form:"comment"`
|
||||
Reset int `json:"reset" form:"reset"`
|
||||
}
|
||||
|
|
|
@ -1,16 +1,131 @@
|
|||
# services:
|
||||
# # mysql1:
|
||||
# # image: mysql:8.0
|
||||
# # container_name: 3xui1_db
|
||||
# # environment:
|
||||
# # MYSQL_ROOT_PASSWORD: your_root_password
|
||||
# # MYSQL_DATABASE: x_ui_db
|
||||
# # MYSQL_USER: x_ui_user
|
||||
# # MYSQL_PASSWORD: your_password
|
||||
# # volumes:
|
||||
# # - mysql_data:/var/lib/mysql
|
||||
# # restart: unless-stopped
|
||||
# # ports:
|
||||
# # - "3307:3306"
|
||||
|
||||
# 3xui1:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ./Dockerfile
|
||||
# container_name: 3xui1_app
|
||||
# volumes:
|
||||
# - $PWD/cert/:/root/cert/
|
||||
# environment:
|
||||
# XRAY_VMESS_AEAD_FORCED: "false"
|
||||
# XUI_ENABLE_FAIL2BAN: "true"
|
||||
# DB_HOST:
|
||||
# DB_PORT: 3306
|
||||
# DB_NAME: x_ui_db
|
||||
# DB_USER: x_ui_user
|
||||
# DB_PASSWORD: your_password
|
||||
# tty: true
|
||||
# # depends_on:
|
||||
# # - mysql
|
||||
# networks:
|
||||
# - default
|
||||
|
||||
# ports:
|
||||
# - "20531:2053"
|
||||
# - "20541:2054"
|
||||
# restart: unless-stopped
|
||||
|
||||
# volumes:
|
||||
# mysql_data:
|
||||
|
||||
|
||||
# networks:
|
||||
# default:
|
||||
# driver: bridge
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
3xui:
|
||||
mysql1:
|
||||
image: mysql:8.0
|
||||
container_name: 3xui1_db
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: your_root_password
|
||||
MYSQL_DATABASE: x_ui_db
|
||||
MYSQL_USER: x_ui_user
|
||||
MYSQL_PASSWORD: your_password
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3307:3306"
|
||||
networks:
|
||||
- default
|
||||
|
||||
3xui1:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
container_name: 3xui_app
|
||||
# hostname: yourhostname <- optional
|
||||
container_name: 3xui1_app
|
||||
volumes:
|
||||
- $PWD/db/:/etc/x-ui/
|
||||
- $PWD/cert/:/root/cert/
|
||||
- ./cert/:/root/cert/
|
||||
environment:
|
||||
XRAY_VMESS_AEAD_FORCED: "false"
|
||||
XUI_ENABLE_FAIL2BAN: "true"
|
||||
DB_HOST: mysql1
|
||||
DB_PORT: 3306
|
||||
DB_NAME: x_ui_db
|
||||
DB_USER: x_ui_user
|
||||
DB_PASSWORD: your_password
|
||||
tty: true
|
||||
network_mode: host
|
||||
depends_on:
|
||||
- mysql1
|
||||
networks:
|
||||
- default
|
||||
ports:
|
||||
- "20531:2053"
|
||||
- "20541:2054"
|
||||
restart: unless-stopped
|
||||
|
||||
ldap:
|
||||
image: osixia/openldap:1.5.0
|
||||
container_name: ldap_server
|
||||
environment:
|
||||
LDAP_ORGANISATION: "TestOrg"
|
||||
LDAP_DOMAIN: "test.local"
|
||||
LDAP_ADMIN_PASSWORD: admin
|
||||
ports:
|
||||
- "389:389"
|
||||
- "636:636"
|
||||
volumes:
|
||||
- ldap_data:/var/lib/ldap
|
||||
- ldap_config:/etc/ldap/slapd.d
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- default
|
||||
|
||||
phpldapadmin:
|
||||
image: osixia/phpldapadmin:0.9.0
|
||||
container_name: ldap_admin
|
||||
environment:
|
||||
PHPLDAPADMIN_LDAP_HOSTS: ldap
|
||||
PHPLDAPADMIN_HTTPS: "false"
|
||||
ports:
|
||||
- "8080:80"
|
||||
depends_on:
|
||||
- ldap
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- default
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
ldap_data:
|
||||
ldap_config:
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
|
|
77
go.mod
77
go.mod
|
@ -1,49 +1,52 @@
|
|||
module github.com/mhsanaei/3x-ui/v2
|
||||
module x-ui
|
||||
|
||||
go 1.25.1
|
||||
go 1.24
|
||||
|
||||
toolchain go1.24.6
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/gzip v1.2.3
|
||||
github.com/gin-contrib/sessions v1.0.4
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-ldap/ldap/v3 v3.4.7
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mymmrac/telego v1.3.0
|
||||
github.com/mymmrac/telego v0.32.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.8
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/valyala/fasthttp v1.66.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.7
|
||||
github.com/valyala/fasthttp v1.64.0
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
github.com/xtls/xray-core v1.250911.0
|
||||
github.com/xtls/xray-core v1.250803.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/sys v0.36.0
|
||||
golang.org/x/text v0.29.0
|
||||
google.golang.org/grpc v1.75.1
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.0
|
||||
golang.org/x/crypto v0.40.0
|
||||
golang.org/x/text v0.27.0
|
||||
google.golang.org/grpc v1.74.2
|
||||
gorm.io/driver/mysql v1.5.2
|
||||
gorm.io/gorm v1.30.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.1 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/fasthttp/router v1.5.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
|
@ -58,9 +61,8 @@ require (
|
|||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
|
@ -71,8 +73,9 @@ require (
|
|||
github.com/refraction-networking/utls v1.8.0 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sagernet/sing v0.7.10 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
||||
github.com/sagernet/sing v0.6.6 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
|
@ -83,20 +86,22 @@ require (
|
|||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/vishvananda/netlink v1.3.1 // 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-20250727231020-de3bb4d08f5a // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.21.0 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.org/x/arch v0.19.0 // indirect
|
||||
golang.org/x/mod v0.26.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.35.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
)
|
||||
|
|
242
go.sum
242
go.sum
|
@ -1,17 +1,21 @@
|
|||
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/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
|
@ -19,10 +23,12 @@ 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-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||
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/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/fasthttp/router v1.5.4 h1:oxdThbBwQgsDIYZ3wR1IavsNl6ZS9WdjKukeMikOnC8=
|
||||
github.com/fasthttp/router v1.5.4/go.mod h1:3/hysWq6cky7dTfzaaEPZGdptwjwx0qzTgFCKEWRjgc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U=
|
||||
|
@ -31,8 +37,12 @@ github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kb
|
|||
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.7 h1:3Hbd7mIB1qjd3Ra59fI3JYea/t5kykFu2CVHBca9koE=
|
||||
github.com/go-ldap/ldap/v3 v3.4.7/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
|
@ -48,10 +58,10 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.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/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
|
@ -67,14 +77,31 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
|
||||
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
|
@ -87,20 +114,20 @@ github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
|||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
@ -108,8 +135,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mymmrac/telego v1.3.0 h1:y2bDDCioLgkcs+5luUaPgTNHKel1Qh30iUxFcMUrowg=
|
||||
github.com/mymmrac/telego v1.3.0/go.mod h1:0D2l/IA/gUFn4oqsi1O4/tSnlezw5jNV/ReFRDUEKk8=
|
||||
github.com/mymmrac/telego v0.32.0 h1:4X8C1l3k+opkk86r95+eQE8DxiS2LYlR61L/G7yreDY=
|
||||
github.com/mymmrac/telego v0.32.0/go.mod h1:qS6NaRhJgcuEEBEMVCV79S2xCAuHq9O+ixwfLuRW31M=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
|
@ -136,26 +163,28 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
|||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sagernet/sing v0.7.10 h1:2yPhZFx+EkyHPH8hXNezgyRSHyGY12CboId7CtwLROw=
|
||||
github.com/sagernet/sing v0.7.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
||||
github.com/sagernet/sing v0.6.6 h1:3JkvJ0vqDj/jJcx0a+ve/6lMOrSzZm30I3wrIuZtmRE=
|
||||
github.com/sagernet/sing v0.6.6/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
|
||||
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 h1:qIQ0tWF9vxGtkJa24bR+2i53WBCz1nW/Pc47oVYauC4=
|
||||
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
|
||||
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
|
@ -168,8 +197,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
|
|||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNuyU=
|
||||
github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs=
|
||||
github.com/valyala/fasthttp v1.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og=
|
||||
github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA=
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
|
@ -178,84 +207,129 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
|
|||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
||||
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c h1:LHLhQY3mKXSpTcQAkjFR4/6ar3rXjQryNeM7khK3AHU=
|
||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
|
||||
github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno=
|
||||
github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k=
|
||||
github.com/xtls/reality v0.0.0-20250727231020-de3bb4d08f5a h1:Fs8Pc0JAc/LDOf9Q4DzKrk+Ujf4ILlyvfvDVZcmOZ2o=
|
||||
github.com/xtls/reality v0.0.0-20250727231020-de3bb4d08f5a/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
|
||||
github.com/xtls/xray-core v1.250803.0 h1:sYdRC243UsujnePINH4IfM4MfHE4lj2p4wZFAfeE2GI=
|
||||
github.com/xtls/xray-core v1.250803.0/go.mod h1:z2vn2o30flYEgpSz1iEhdZP1I46UZ3+gXINZyohH3yE=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
|
||||
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
|
||||
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.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.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
|
||||
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-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||
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/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
|
||||
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
|
||||
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
|
||||
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
|
||||
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h1:sfK5nHuG7lRFZ2FdTT3RimOqWBg8IrVm+/Vko1FVOsk=
|
||||
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
|
||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
|
66
install.sh
66
install.sh
|
@ -7,6 +7,7 @@ yellow='\033[0;33m'
|
|||
plain='\033[0m'
|
||||
|
||||
cur_dir=$(pwd)
|
||||
show_ip_service_lists=("https://api.ipify.org" "https://4.ident.me")
|
||||
|
||||
# check root
|
||||
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1
|
||||
|
@ -56,11 +57,8 @@ install_base() {
|
|||
opensuse-tumbleweed)
|
||||
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 install -y -q wget curl tar tzdata
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
@ -75,18 +73,10 @@ config_after_install() {
|
|||
local existing_hasDefaultCredential=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}')
|
||||
local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
|
||||
local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
||||
local URL_lists=(
|
||||
"https://api4.ipify.org"
|
||||
"https://ipv4.icanhazip.com"
|
||||
"https://v4.api.ipinfo.io/ip"
|
||||
"https://ipv4.myexternalip.com/raw"
|
||||
"https://4.ident.me"
|
||||
"https://check-host.net/ip"
|
||||
)
|
||||
local server_ip=""
|
||||
for ip_address in "${URL_lists[@]}"; do
|
||||
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
||||
if [[ -n "${server_ip}" ]]; then
|
||||
|
||||
for ip_service_addr in "${show_ip_service_lists[@]}"; do
|
||||
local server_ip=$(curl -s --max-time 3 ${ip_service_addr} 2>/dev/null)
|
||||
if [ -n "${server_ip}" ]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
@ -149,15 +139,11 @@ install_x-ui() {
|
|||
if [ $# == 0 ]; then
|
||||
tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$tag_version" ]]; then
|
||||
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
|
||||
tag_version=$(curl -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$tag_version" ]]; then
|
||||
echo -e "${red}Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later${plain}"
|
||||
exit 1
|
||||
fi
|
||||
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
|
||||
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
|
||||
wget --inet4-only -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz
|
||||
wget -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}"
|
||||
exit 1
|
||||
|
@ -174,25 +160,17 @@ install_x-ui() {
|
|||
|
||||
url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz"
|
||||
echo -e "Beginning to install x-ui $1"
|
||||
wget --inet4-only -N -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
|
||||
wget -N -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
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
|
||||
wget -O /usr/bin/x-ui-temp https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
|
||||
|
||||
# Stop x-ui service and remove old resources
|
||||
if [[ -e /usr/local/x-ui/ ]]; then
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui stop
|
||||
else
|
||||
systemctl stop x-ui
|
||||
fi
|
||||
systemctl stop x-ui
|
||||
rm /usr/local/x-ui/ -rf
|
||||
fi
|
||||
|
||||
|
@ -216,22 +194,10 @@ install_x-ui() {
|
|||
chmod +x /usr/bin/x-ui
|
||||
config_after_install
|
||||
|
||||
if [[ $release == "alpine" ]]; then
|
||||
wget --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}Failed to download x-ui.rc${plain}"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x /etc/init.d/x-ui
|
||||
rc-update add x-ui
|
||||
rc-service x-ui start
|
||||
else
|
||||
cp -f x-ui.service /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl start x-ui
|
||||
fi
|
||||
|
||||
cp -f x-ui.service /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl start x-ui
|
||||
echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
|
||||
echo -e ""
|
||||
echo -e "┌───────────────────────────────────────────────────────┐
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// Package logger provides logging functionality for the 3x-ui panel with
|
||||
// buffered log storage and multiple log levels.
|
||||
package logger
|
||||
|
||||
import (
|
||||
|
@ -11,11 +9,7 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
logger *logging.Logger
|
||||
|
||||
// addToBuffer appends a log entry into the in-memory ring buffer used for
|
||||
// retrieving recent logs via the web UI. It keeps the buffer bounded to avoid
|
||||
// uncontrolled growth.
|
||||
logger *logging.Logger
|
||||
logBuffer []struct {
|
||||
time string
|
||||
level logging.Level
|
||||
|
@ -27,7 +21,6 @@ func init() {
|
|||
InitLogger(logging.INFO)
|
||||
}
|
||||
|
||||
// InitLogger initializes the logger with the specified logging level.
|
||||
func InitLogger(level logging.Level) {
|
||||
newLogger := logging.MustGetLogger("x-ui")
|
||||
var err error
|
||||
|
@ -54,61 +47,51 @@ func InitLogger(level logging.Level) {
|
|||
logger = newLogger
|
||||
}
|
||||
|
||||
// Debug logs a debug message and adds it to the log buffer.
|
||||
func Debug(args ...any) {
|
||||
logger.Debug(args...)
|
||||
addToBuffer("DEBUG", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Debugf logs a formatted debug message and adds it to the log buffer.
|
||||
func Debugf(format string, args ...any) {
|
||||
logger.Debugf(format, args...)
|
||||
addToBuffer("DEBUG", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Info logs an info message and adds it to the log buffer.
|
||||
func Info(args ...any) {
|
||||
logger.Info(args...)
|
||||
addToBuffer("INFO", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Infof logs a formatted info message and adds it to the log buffer.
|
||||
func Infof(format string, args ...any) {
|
||||
logger.Infof(format, args...)
|
||||
addToBuffer("INFO", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Notice logs a notice message and adds it to the log buffer.
|
||||
func Notice(args ...any) {
|
||||
logger.Notice(args...)
|
||||
addToBuffer("NOTICE", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Noticef logs a formatted notice message and adds it to the log buffer.
|
||||
func Noticef(format string, args ...any) {
|
||||
logger.Noticef(format, args...)
|
||||
addToBuffer("NOTICE", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Warning logs a warning message and adds it to the log buffer.
|
||||
func Warning(args ...any) {
|
||||
logger.Warning(args...)
|
||||
addToBuffer("WARNING", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Warningf logs a formatted warning message and adds it to the log buffer.
|
||||
func Warningf(format string, args ...any) {
|
||||
logger.Warningf(format, args...)
|
||||
addToBuffer("WARNING", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Error logs an error message and adds it to the log buffer.
|
||||
func Error(args ...any) {
|
||||
logger.Error(args...)
|
||||
addToBuffer("ERROR", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Errorf logs a formatted error message and adds it to the log buffer.
|
||||
func Errorf(format string, args ...any) {
|
||||
logger.Errorf(format, args...)
|
||||
addToBuffer("ERROR", fmt.Sprintf(format, args...))
|
||||
|
@ -132,7 +115,6 @@ func addToBuffer(level string, newLog string) {
|
|||
})
|
||||
}
|
||||
|
||||
// GetLogs retrieves up to c log entries from the buffer that are at or below the specified level.
|
||||
func GetLogs(c int, level string) []string {
|
||||
var output []string
|
||||
logLevel, _ := logging.LogLevel(level)
|
||||
|
|
32
main.go
32
main.go
|
@ -1,5 +1,3 @@
|
|||
// Package main is the entry point for the 3x-ui web panel application.
|
||||
// It initializes the database, web server, and handles command-line operations for managing the panel.
|
||||
package main
|
||||
|
||||
import (
|
||||
|
@ -11,20 +9,19 @@ import (
|
|||
"syscall"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
"github.com/mhsanaei/3x-ui/v2/database"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/sub"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||
"github.com/mhsanaei/3x-ui/v2/web"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/global"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
"x-ui/logger"
|
||||
"x-ui/sub"
|
||||
"x-ui/util/crypto"
|
||||
"x-ui/web"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
// runWebServer initializes and starts the web server for the 3x-ui panel.
|
||||
func runWebServer() {
|
||||
log.Printf("Starting %v %v", config.GetName(), config.GetVersion())
|
||||
|
||||
|
@ -35,7 +32,7 @@ func runWebServer() {
|
|||
logger.InitLogger(logging.INFO)
|
||||
case config.Notice:
|
||||
logger.InitLogger(logging.NOTICE)
|
||||
case config.Warning:
|
||||
case config.Warn:
|
||||
logger.InitLogger(logging.WARNING)
|
||||
case config.Error:
|
||||
logger.InitLogger(logging.ERROR)
|
||||
|
@ -114,7 +111,6 @@ func runWebServer() {
|
|||
}
|
||||
}
|
||||
|
||||
// resetSetting resets all panel settings to their default values.
|
||||
func resetSetting() {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
|
@ -131,7 +127,6 @@ func resetSetting() {
|
|||
}
|
||||
}
|
||||
|
||||
// showSetting displays the current panel settings if show is true.
|
||||
func showSetting(show bool) {
|
||||
if show {
|
||||
settingService := service.SettingService{}
|
||||
|
@ -181,7 +176,6 @@ func showSetting(show bool) {
|
|||
}
|
||||
}
|
||||
|
||||
// updateTgbotEnableSts enables or disables the Telegram bot notifications based on the status parameter.
|
||||
func updateTgbotEnableSts(status bool) {
|
||||
settingService := service.SettingService{}
|
||||
currentTgSts, err := settingService.GetTgbotEnabled()
|
||||
|
@ -201,7 +195,6 @@ func updateTgbotEnableSts(status bool) {
|
|||
}
|
||||
}
|
||||
|
||||
// updateTgbotSetting updates Telegram bot settings including token, chat ID, and runtime schedule.
|
||||
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
|
@ -239,7 +232,6 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
|
|||
}
|
||||
}
|
||||
|
||||
// updateSetting updates various panel settings including port, credentials, base path, listen IP, and two-factor authentication.
|
||||
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
|
@ -298,7 +290,6 @@ func updateSetting(port int, username string, password string, webBasePath strin
|
|||
}
|
||||
}
|
||||
|
||||
// updateCert updates the SSL certificate files for the panel.
|
||||
func updateCert(publicKey string, privateKey string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
|
@ -326,7 +317,6 @@ func updateCert(publicKey string, privateKey string) {
|
|||
}
|
||||
}
|
||||
|
||||
// GetCertificate displays the current SSL certificate settings if getCert is true.
|
||||
func GetCertificate(getCert bool) {
|
||||
if getCert {
|
||||
settingService := service.SettingService{}
|
||||
|
@ -344,7 +334,6 @@ func GetCertificate(getCert bool) {
|
|||
}
|
||||
}
|
||||
|
||||
// GetListenIP displays the current panel listen IP address if getListen is true.
|
||||
func GetListenIP(getListen bool) {
|
||||
if getListen {
|
||||
|
||||
|
@ -359,7 +348,6 @@ func GetListenIP(getListen bool) {
|
|||
}
|
||||
}
|
||||
|
||||
// migrateDb performs database migration operations for the 3x-ui panel.
|
||||
func migrateDb() {
|
||||
inboundService := service.InboundService{}
|
||||
|
||||
|
@ -372,8 +360,6 @@ func migrateDb() {
|
|||
fmt.Println("Migration done!")
|
||||
}
|
||||
|
||||
// main is the entry point of the 3x-ui application.
|
||||
// It parses command-line arguments to run the web server, migrate database, or update settings.
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
runWebServer()
|
||||
|
|
BIN
media/buymeacoffe.png
Normal file
BIN
media/buymeacoffe.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
|
@ -13,7 +13,7 @@
|
|||
"inbounds": [
|
||||
{
|
||||
"port": 10808,
|
||||
"protocol": "mixed",
|
||||
"protocol": "socks",
|
||||
"settings": {
|
||||
"auth": "noauth",
|
||||
"udp": true,
|
||||
|
@ -28,7 +28,7 @@
|
|||
],
|
||||
"enabled": true
|
||||
},
|
||||
"tag": "mixed"
|
||||
"tag": "socks"
|
||||
},
|
||||
{
|
||||
"port": 10809,
|
||||
|
|
170
sub/sub.go
170
sub/sub.go
|
@ -1,47 +1,23 @@
|
|||
// Package sub provides subscription server functionality for the 3x-ui panel,
|
||||
// including HTTP/HTTPS servers for serving subscription links and JSON configurations.
|
||||
package sub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||
webpkg "github.com/mhsanaei/3x-ui/v2/web"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/locale"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/middleware"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/network"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/middleware"
|
||||
"x-ui/web/network"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// setEmbeddedTemplates parses and sets embedded templates on the engine
|
||||
func setEmbeddedTemplates(engine *gin.Engine) error {
|
||||
t, err := template.New("").Funcs(engine.FuncMap).ParseFS(
|
||||
webpkg.EmbeddedHTML(),
|
||||
"html/common/page.html",
|
||||
"html/component/aThemeSwitch.html",
|
||||
"html/settings/panel/subscription/subpage.html",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
engine.SetHTMLTemplate(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Server represents the subscription server that serves subscription links and JSON configurations.
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
|
@ -53,7 +29,6 @@ type Server struct {
|
|||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// NewServer creates a new subscription server instance with a cancellable context.
|
||||
func NewServer() *Server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Server{
|
||||
|
@ -62,13 +37,14 @@ func NewServer() *Server {
|
|||
}
|
||||
}
|
||||
|
||||
// initRouter configures the subscription server's Gin engine, middleware,
|
||||
// templates and static assets and returns the ready-to-use engine.
|
||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
// Always run in release mode for the subscription server
|
||||
gin.DefaultWriter = io.Discard
|
||||
gin.DefaultErrorWriter = io.Discard
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
if config.IsDebug() {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
gin.DefaultWriter = io.Discard
|
||||
gin.DefaultErrorWriter = io.Discard
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
engine := gin.Default()
|
||||
|
||||
|
@ -91,23 +67,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Determine if JSON subscription endpoint is enabled
|
||||
subJsonEnable, err := s.settingService.GetSubJsonEnable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set base_path based on LinksPath for template rendering
|
||||
// Ensure LinksPath ends with "/" for proper asset URL generation
|
||||
basePath := LinksPath
|
||||
if basePath != "/" && !strings.HasSuffix(basePath, "/") {
|
||||
basePath += "/"
|
||||
}
|
||||
logger.Debug("sub: Setting base_path to:", basePath)
|
||||
engine.Use(func(c *gin.Context) {
|
||||
c.Set("base_path", basePath)
|
||||
})
|
||||
|
||||
Encrypt, err := s.settingService.GetSubEncrypt()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -153,114 +112,15 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
SubTitle = ""
|
||||
}
|
||||
|
||||
// set per-request localizer from headers/cookies
|
||||
engine.Use(locale.LocalizerMiddleware())
|
||||
|
||||
// register i18n function similar to web server
|
||||
i18nWebFunc := func(key string, params ...string) string {
|
||||
return locale.I18n(locale.Web, key, params...)
|
||||
}
|
||||
engine.SetFuncMap(map[string]any{"i18n": i18nWebFunc})
|
||||
|
||||
// Templates: prefer embedded; fallback to disk if necessary
|
||||
if err := setEmbeddedTemplates(engine); err != nil {
|
||||
logger.Warning("sub: failed to parse embedded templates:", err)
|
||||
if files, derr := s.getHtmlFiles(); derr == nil {
|
||||
engine.LoadHTMLFiles(files...)
|
||||
} else {
|
||||
logger.Error("sub: no templates available (embedded parse and disk load failed)", err, derr)
|
||||
}
|
||||
}
|
||||
|
||||
// Assets: use disk if present, fallback to embedded
|
||||
// Serve under both root (/assets) and under the subscription path prefix (LinksPath + "assets")
|
||||
// so reverse proxies with a URI prefix can load assets correctly.
|
||||
// Determine LinksPath earlier to compute prefixed assets mount.
|
||||
// Note: LinksPath always starts and ends with "/" (validated in settings).
|
||||
var linksPathForAssets string
|
||||
if LinksPath == "/" {
|
||||
linksPathForAssets = "/assets"
|
||||
} else {
|
||||
// ensure single slash join
|
||||
linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets"
|
||||
}
|
||||
|
||||
// Mount assets in multiple paths to handle different URL patterns
|
||||
var assetsFS http.FileSystem
|
||||
if _, err := os.Stat("web/assets"); err == nil {
|
||||
assetsFS = http.FS(os.DirFS("web/assets"))
|
||||
} else {
|
||||
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
|
||||
assetsFS = http.FS(subFS)
|
||||
} else {
|
||||
logger.Error("sub: failed to mount embedded assets:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if assetsFS != nil {
|
||||
engine.StaticFS("/assets", assetsFS)
|
||||
if linksPathForAssets != "/assets" {
|
||||
engine.StaticFS(linksPathForAssets, assetsFS)
|
||||
}
|
||||
|
||||
// Add middleware to handle dynamic asset paths with subid
|
||||
if LinksPath != "/" {
|
||||
engine.Use(func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
// Check if this is an asset request with subid pattern: /sub/path/{subid}/assets/...
|
||||
pathPrefix := strings.TrimRight(LinksPath, "/") + "/"
|
||||
if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") {
|
||||
// Extract the asset path after /assets/
|
||||
assetsIndex := strings.Index(path, "/assets/")
|
||||
if assetsIndex != -1 {
|
||||
assetPath := path[assetsIndex+8:] // +8 to skip "/assets/"
|
||||
if assetPath != "" {
|
||||
// Serve the asset file
|
||||
c.FileFromFS(assetPath, assetsFS)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
g := engine.Group("/")
|
||||
|
||||
s.sub = NewSUBController(
|
||||
g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||
g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
// getHtmlFiles loads templates from local folder (used in debug mode)
|
||||
func (s *Server) getHtmlFiles() ([]string, error) {
|
||||
dir, _ := os.Getwd()
|
||||
files := []string{}
|
||||
// common layout
|
||||
common := filepath.Join(dir, "web", "html", "common", "page.html")
|
||||
if _, err := os.Stat(common); err == nil {
|
||||
files = append(files, common)
|
||||
}
|
||||
// components used
|
||||
theme := filepath.Join(dir, "web", "html", "component", "aThemeSwitch.html")
|
||||
if _, err := os.Stat(theme); err == nil {
|
||||
files = append(files, theme)
|
||||
}
|
||||
// page itself
|
||||
page := filepath.Join(dir, "web", "html", "subpage.html")
|
||||
if _, err := os.Stat(page); err == nil {
|
||||
files = append(files, page)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// Start initializes and starts the subscription server with configured settings.
|
||||
func (s *Server) Start() (err error) {
|
||||
// This is an anonymous function, no function name
|
||||
defer func() {
|
||||
|
@ -334,7 +194,6 @@ func (s *Server) Start() (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down the subscription server and closes the listener.
|
||||
func (s *Server) Stop() error {
|
||||
s.cancel()
|
||||
|
||||
|
@ -349,7 +208,6 @@ func (s *Server) Stop() error {
|
|||
return common.Combine(err1, err2)
|
||||
}
|
||||
|
||||
// GetCtx returns the server's context for cancellation and deadline management.
|
||||
func (s *Server) GetCtx() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
|
|
|
@ -2,20 +2,16 @@ package sub
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SUBController handles HTTP requests for subscription links and JSON configurations.
|
||||
type SUBController struct {
|
||||
subTitle string
|
||||
subPath string
|
||||
subJsonPath string
|
||||
jsonEnabled bool
|
||||
subEncrypt bool
|
||||
updateInterval string
|
||||
|
||||
|
@ -23,12 +19,10 @@ type SUBController struct {
|
|||
subJsonService *SubJsonService
|
||||
}
|
||||
|
||||
// NewSUBController creates a new subscription controller with the given configuration.
|
||||
func NewSUBController(
|
||||
g *gin.RouterGroup,
|
||||
subPath string,
|
||||
jsonPath string,
|
||||
jsonEnabled bool,
|
||||
encrypt bool,
|
||||
showInfo bool,
|
||||
rModel string,
|
||||
|
@ -44,7 +38,6 @@ func NewSUBController(
|
|||
subTitle: subTitle,
|
||||
subPath: subPath,
|
||||
subJsonPath: jsonPath,
|
||||
jsonEnabled: jsonEnabled,
|
||||
subEncrypt: encrypt,
|
||||
updateInterval: update,
|
||||
|
||||
|
@ -55,22 +48,32 @@ func NewSUBController(
|
|||
return a
|
||||
}
|
||||
|
||||
// initRouter registers HTTP routes for subscription links and JSON endpoints
|
||||
// on the provided router group.
|
||||
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||
gLink := g.Group(a.subPath)
|
||||
gJson := g.Group(a.subJsonPath)
|
||||
|
||||
gLink.GET(":subid", a.subs)
|
||||
if a.jsonEnabled {
|
||||
gJson := g.Group(a.subJsonPath)
|
||||
gJson.GET(":subid", a.subJsons)
|
||||
}
|
||||
|
||||
gJson.GET(":subid", a.subJsons)
|
||||
}
|
||||
|
||||
// subs handles HTTP requests for subscription links, returning either HTML page or base64-encoded subscription data.
|
||||
func (a *SUBController) subs(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c)
|
||||
subs, lastOnline, traffic, err := a.subService.GetSubs(subId, host)
|
||||
var host string
|
||||
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil {
|
||||
host = h
|
||||
}
|
||||
if host == "" {
|
||||
host = c.GetHeader("X-Real-IP")
|
||||
}
|
||||
if host == "" {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(c.Request.Host)
|
||||
if err != nil {
|
||||
host = c.Request.Host
|
||||
}
|
||||
}
|
||||
subs, header, err := a.subService.GetSubs(subId, host)
|
||||
if err != nil || len(subs) == 0 {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
|
@ -79,55 +82,10 @@ func (a *SUBController) subs(c *gin.Context) {
|
|||
result += sub + "\n"
|
||||
}
|
||||
|
||||
// If the request expects HTML (e.g., browser) or explicitly asked (?html=1 or ?view=html), render the info page here
|
||||
accept := c.GetHeader("Accept")
|
||||
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
|
||||
// Build page data in service
|
||||
subURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId)
|
||||
if !a.jsonEnabled {
|
||||
subJsonURL = ""
|
||||
}
|
||||
// Get base_path from context (set by middleware)
|
||||
basePath, exists := c.Get("base_path")
|
||||
if !exists {
|
||||
basePath = "/"
|
||||
}
|
||||
// Add subId to base_path for asset URLs
|
||||
basePathStr := basePath.(string)
|
||||
if basePathStr == "/" {
|
||||
basePathStr = "/" + subId + "/"
|
||||
} else {
|
||||
// Remove trailing slash if exists, add subId, then add trailing slash
|
||||
basePathStr = strings.TrimRight(basePathStr, "/") + "/" + subId + "/"
|
||||
}
|
||||
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL, basePathStr)
|
||||
c.HTML(200, "subpage.html", gin.H{
|
||||
"title": "subscription.title",
|
||||
"cur_ver": config.GetVersion(),
|
||||
"host": page.Host,
|
||||
"base_path": page.BasePath,
|
||||
"sId": page.SId,
|
||||
"download": page.Download,
|
||||
"upload": page.Upload,
|
||||
"total": page.Total,
|
||||
"used": page.Used,
|
||||
"remained": page.Remained,
|
||||
"expire": page.Expire,
|
||||
"lastOnline": page.LastOnline,
|
||||
"datepicker": page.Datepicker,
|
||||
"downloadByte": page.DownloadByte,
|
||||
"uploadByte": page.UploadByte,
|
||||
"totalByte": page.TotalByte,
|
||||
"subUrl": page.SubUrl,
|
||||
"subJsonUrl": page.SubJsonUrl,
|
||||
"result": page.Result,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Add headers
|
||||
header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
||||
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
|
||||
|
||||
if a.subEncrypt {
|
||||
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||
|
@ -137,25 +95,43 @@ func (a *SUBController) subs(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// subJsons handles HTTP requests for JSON subscription configurations.
|
||||
func (a *SUBController) subJsons(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
_, host, _, _ := a.subService.ResolveRequest(c)
|
||||
var host string
|
||||
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil {
|
||||
host = h
|
||||
}
|
||||
if host == "" {
|
||||
host = c.GetHeader("X-Real-IP")
|
||||
}
|
||||
if host == "" {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(c.Request.Host)
|
||||
if err != nil {
|
||||
host = c.Request.Host
|
||||
}
|
||||
}
|
||||
jsonSub, header, err := a.subJsonService.GetJson(subId, host)
|
||||
if err != nil || len(jsonSub) == 0 {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
|
||||
// Add headers
|
||||
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
|
||||
|
||||
c.String(200, jsonSub)
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title.
|
||||
func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle)))
|
||||
func getHostFromXFH(s string) (string, error) {
|
||||
if strings.Contains(s, ":") {
|
||||
realHost, _, err := net.SplitHostPort(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return realHost, nil
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
|
|
@ -6,18 +6,17 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/random"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/json_util"
|
||||
"x-ui/util/random"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
)
|
||||
|
||||
//go:embed default.json
|
||||
var defaultJson string
|
||||
|
||||
// SubJsonService handles JSON subscription configuration generation and management.
|
||||
type SubJsonService struct {
|
||||
configJson map[string]any
|
||||
defaultOutbounds []json_util.RawMessage
|
||||
|
@ -29,7 +28,6 @@ type SubJsonService struct {
|
|||
SubService *SubService
|
||||
}
|
||||
|
||||
// NewSubJsonService creates a new JSON subscription service with the given configuration.
|
||||
func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService {
|
||||
var configJson map[string]any
|
||||
var defaultOutbounds []json_util.RawMessage
|
||||
|
@ -69,7 +67,6 @@ func NewSubJsonService(fragment string, noises string, mux string, rules string,
|
|||
}
|
||||
}
|
||||
|
||||
// GetJson generates a JSON subscription configuration for the given subscription ID and host.
|
||||
func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
|
||||
inbounds, err := s.SubService.getInboundsBySubId(subId)
|
||||
if err != nil || len(inbounds) == 0 {
|
||||
|
@ -174,12 +171,12 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
|||
case "tls":
|
||||
if newStream["security"] != "tls" {
|
||||
newStream["security"] = "tls"
|
||||
newStream["tlsSettings"] = map[string]any{}
|
||||
newStream["tslSettings"] = map[string]any{}
|
||||
}
|
||||
case "none":
|
||||
if newStream["security"] != "none" {
|
||||
newStream["security"] = "none"
|
||||
delete(newStream, "tlsSettings")
|
||||
delete(newStream, "tslSettings")
|
||||
}
|
||||
}
|
||||
streamSettings, _ := json.MarshalIndent(newStream, "", " ")
|
||||
|
@ -187,10 +184,8 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
|||
var newOutbounds []json_util.RawMessage
|
||||
|
||||
switch inbound.Protocol {
|
||||
case "vmess":
|
||||
case "vmess", "vless":
|
||||
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client))
|
||||
case "vless":
|
||||
newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client))
|
||||
case "trojan", "shadowsocks":
|
||||
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
|
||||
}
|
||||
|
@ -214,10 +209,9 @@ func (s *SubJsonService) streamData(stream string) map[string]any {
|
|||
var streamSettings map[string]any
|
||||
json.Unmarshal([]byte(stream), &streamSettings)
|
||||
security, _ := streamSettings["security"].(string)
|
||||
switch security {
|
||||
case "tls":
|
||||
if security == "tls" {
|
||||
streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any))
|
||||
case "reality":
|
||||
} else if security == "reality" {
|
||||
streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any))
|
||||
}
|
||||
delete(streamSettings, "sockopt")
|
||||
|
@ -294,8 +288,15 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
|
|||
usersData := make([]UserVnext, 1)
|
||||
|
||||
usersData[0].ID = client.ID
|
||||
usersData[0].Email = client.Email
|
||||
usersData[0].Security = client.Security
|
||||
usersData[0].Level = 8
|
||||
if inbound.Protocol == model.VMESS {
|
||||
usersData[0].Security = client.Security
|
||||
}
|
||||
if inbound.Protocol == model.VLESS {
|
||||
usersData[0].Flow = client.Flow
|
||||
usersData[0].Encryption = "none"
|
||||
}
|
||||
|
||||
vnextData := make([]VnextSetting, 1)
|
||||
vnextData[0] = VnextSetting{
|
||||
Address: inbound.Listen,
|
||||
|
@ -309,42 +310,14 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
|
|||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
outbound.Settings = map[string]any{
|
||||
"vnext": vnextData,
|
||||
outbound.Settings = OutboundSettings{
|
||||
Vnext: vnextData,
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||
outbound := Outbound{}
|
||||
outbound.Protocol = string(inbound.Protocol)
|
||||
outbound.Tag = "proxy"
|
||||
if s.mux != "" {
|
||||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
settings := make(map[string]any)
|
||||
settings["address"] = inbound.Listen
|
||||
settings["port"] = inbound.Port
|
||||
settings["id"] = client.ID
|
||||
if client.Flow != "" {
|
||||
settings["flow"] = client.Flow
|
||||
}
|
||||
|
||||
// Add encryption for VLESS outbound from inbound settings
|
||||
var inboundSettings map[string]any
|
||||
json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
|
||||
if encryption, ok := inboundSettings["encryption"].(string); ok {
|
||||
settings["encryption"] = encryption
|
||||
}
|
||||
|
||||
outbound.Settings = settings
|
||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||
outbound := Outbound{}
|
||||
|
||||
|
@ -376,8 +349,8 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
|
|||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
outbound.Settings = map[string]any{
|
||||
"servers": serverData,
|
||||
outbound.Settings = OutboundSettings{
|
||||
Servers: serverData,
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||
|
@ -389,7 +362,13 @@ type Outbound struct {
|
|||
Tag string `json:"tag"`
|
||||
StreamSettings json_util.RawMessage `json:"streamSettings"`
|
||||
Mux json_util.RawMessage `json:"mux,omitempty"`
|
||||
Settings map[string]any `json:"settings,omitempty"`
|
||||
ProxySettings map[string]any `json:"proxySettings,omitempty"`
|
||||
Settings OutboundSettings `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
type OutboundSettings struct {
|
||||
Vnext []VnextSetting `json:"vnext,omitempty"`
|
||||
Servers []ServerSetting `json:"servers,omitempty"`
|
||||
}
|
||||
|
||||
type VnextSetting struct {
|
||||
|
@ -399,9 +378,11 @@ type VnextSetting struct {
|
|||
}
|
||||
|
||||
type UserVnext struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Security string `json:"security,omitempty"`
|
||||
Encryption string `json:"encryption,omitempty"`
|
||||
Flow string `json:"flow,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Security string `json:"security,omitempty"`
|
||||
Level int `json:"level"`
|
||||
}
|
||||
|
||||
type ServerSetting struct {
|
||||
|
|
|
@ -3,24 +3,21 @@ package sub
|
|||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/goccy/go-json"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/util/random"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/database"
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/random"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
// SubService provides business logic for generating subscription links and managing subscription data.
|
||||
type SubService struct {
|
||||
address string
|
||||
showInfo bool
|
||||
|
@ -30,7 +27,6 @@ type SubService struct {
|
|||
settingService service.SettingService
|
||||
}
|
||||
|
||||
// NewSubService creates a new subscription service with the given configuration.
|
||||
func NewSubService(showInfo bool, remarkModel string) *SubService {
|
||||
return &SubService{
|
||||
showInfo: showInfo,
|
||||
|
@ -38,20 +34,19 @@ func NewSubService(showInfo bool, remarkModel string) *SubService {
|
|||
}
|
||||
}
|
||||
|
||||
// GetSubs retrieves subscription links for a given subscription ID and host.
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.ClientTraffic, error) {
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
|
||||
s.address = host
|
||||
var result []string
|
||||
var header string
|
||||
var traffic xray.ClientTraffic
|
||||
var lastOnline int64
|
||||
var clientTraffics []xray.ClientTraffic
|
||||
inbounds, err := s.getInboundsBySubId(subId)
|
||||
if err != nil {
|
||||
return nil, 0, traffic, err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if len(inbounds) == 0 {
|
||||
return nil, 0, traffic, common.NewError("No inbounds found with ", subId)
|
||||
return nil, "", common.NewError("No inbounds found with ", subId)
|
||||
}
|
||||
|
||||
s.datepicker, err = s.settingService.GetDatepicker()
|
||||
|
@ -78,11 +73,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
|
|||
if client.Enable && client.SubID == subId {
|
||||
link := s.getLink(inbound, client.Email)
|
||||
result = append(result, link)
|
||||
ct := s.getClientTraffics(inbound.ClientStats, client.Email)
|
||||
clientTraffics = append(clientTraffics, ct)
|
||||
if ct.LastOnline > lastOnline {
|
||||
lastOnline = ct.LastOnline
|
||||
}
|
||||
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +100,8 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
|
|||
}
|
||||
}
|
||||
}
|
||||
return result, lastOnline, traffic, nil
|
||||
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
||||
return result, header, nil
|
||||
}
|
||||
|
||||
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||
|
@ -337,13 +329,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||
params := make(map[string]string)
|
||||
params["type"] = streamNetwork
|
||||
|
||||
// Add encryption parameter for VLESS from inbound settings
|
||||
var settings map[string]any
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
if encryption, ok := settings["encryption"].(string); ok {
|
||||
params["encryption"] = encryption
|
||||
}
|
||||
|
||||
switch streamNetwork {
|
||||
case "tcp":
|
||||
tcp, _ := stream["tcpSettings"].(map[string]any)
|
||||
|
@ -1010,189 +995,3 @@ func searchHost(headers any) string {
|
|||
|
||||
return ""
|
||||
}
|
||||
|
||||
// PageData is a view model for subpage.html
|
||||
// PageData contains data for rendering the subscription information page.
|
||||
type PageData struct {
|
||||
Host string
|
||||
BasePath string
|
||||
SId string
|
||||
Download string
|
||||
Upload string
|
||||
Total string
|
||||
Used string
|
||||
Remained string
|
||||
Expire int64
|
||||
LastOnline int64
|
||||
Datepicker string
|
||||
DownloadByte int64
|
||||
UploadByte int64
|
||||
TotalByte int64
|
||||
SubUrl string
|
||||
SubJsonUrl string
|
||||
Result []string
|
||||
}
|
||||
|
||||
// ResolveRequest extracts scheme and host info from request/headers consistently.
|
||||
// ResolveRequest extracts scheme, host, and header information from an HTTP request.
|
||||
func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) {
|
||||
// scheme
|
||||
scheme = "http"
|
||||
if c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
// base host (no port)
|
||||
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil && h != "" {
|
||||
host = h
|
||||
}
|
||||
if host == "" {
|
||||
host = c.GetHeader("X-Real-IP")
|
||||
}
|
||||
if host == "" {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(c.Request.Host)
|
||||
if err != nil {
|
||||
host = c.Request.Host
|
||||
}
|
||||
}
|
||||
|
||||
// host:port for URLs
|
||||
hostWithPort = c.GetHeader("X-Forwarded-Host")
|
||||
if hostWithPort == "" {
|
||||
hostWithPort = c.Request.Host
|
||||
}
|
||||
if hostWithPort == "" {
|
||||
hostWithPort = host
|
||||
}
|
||||
|
||||
// header display host
|
||||
hostHeader = c.GetHeader("X-Forwarded-Host")
|
||||
if hostHeader == "" {
|
||||
hostHeader = c.GetHeader("X-Real-IP")
|
||||
}
|
||||
if hostHeader == "" {
|
||||
hostHeader = host
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// BuildURLs constructs absolute subscription and JSON subscription URLs for a given subscription ID.
|
||||
// It prioritizes configured URIs, then individual settings, and finally falls back to request-derived components.
|
||||
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) {
|
||||
// Input validation
|
||||
if subId == "" {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// Get configured URIs first (highest priority)
|
||||
configuredSubURI, _ := s.settingService.GetSubURI()
|
||||
configuredSubJsonURI, _ := s.settingService.GetSubJsonURI()
|
||||
|
||||
// Determine base scheme and host (cached to avoid duplicate calls)
|
||||
var baseScheme, baseHostWithPort string
|
||||
if configuredSubURI == "" || configuredSubJsonURI == "" {
|
||||
baseScheme, baseHostWithPort = s.getBaseSchemeAndHost(scheme, hostWithPort)
|
||||
}
|
||||
|
||||
// Build subscription URL
|
||||
subURL = s.buildSingleURL(configuredSubURI, baseScheme, baseHostWithPort, subPath, subId)
|
||||
|
||||
// Build JSON subscription URL
|
||||
subJsonURL = s.buildSingleURL(configuredSubJsonURI, baseScheme, baseHostWithPort, subJsonPath, subId)
|
||||
|
||||
return subURL, subJsonURL
|
||||
}
|
||||
|
||||
// getBaseSchemeAndHost determines the base scheme and host from settings or falls back to request values
|
||||
func (s *SubService) getBaseSchemeAndHost(requestScheme, requestHostWithPort string) (string, string) {
|
||||
subDomain, err := s.settingService.GetSubDomain()
|
||||
if err != nil || subDomain == "" {
|
||||
return requestScheme, requestHostWithPort
|
||||
}
|
||||
|
||||
// Get port and TLS settings
|
||||
subPort, _ := s.settingService.GetSubPort()
|
||||
subKeyFile, _ := s.settingService.GetSubKeyFile()
|
||||
subCertFile, _ := s.settingService.GetSubCertFile()
|
||||
|
||||
// Determine scheme from TLS configuration
|
||||
scheme := "http"
|
||||
if subKeyFile != "" && subCertFile != "" {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
// Build host:port, always include port for clarity
|
||||
hostWithPort := fmt.Sprintf("%s:%d", subDomain, subPort)
|
||||
|
||||
return scheme, hostWithPort
|
||||
}
|
||||
|
||||
// buildSingleURL constructs a single URL using configured URI or base components
|
||||
func (s *SubService) buildSingleURL(configuredURI, baseScheme, baseHostWithPort, basePath, subId string) string {
|
||||
if configuredURI != "" {
|
||||
return s.joinPathWithID(configuredURI, subId)
|
||||
}
|
||||
|
||||
baseURL := fmt.Sprintf("%s://%s", baseScheme, baseHostWithPort)
|
||||
return s.joinPathWithID(baseURL+basePath, subId)
|
||||
}
|
||||
|
||||
// joinPathWithID safely joins a base path with a subscription ID
|
||||
func (s *SubService) joinPathWithID(basePath, subId string) string {
|
||||
if strings.HasSuffix(basePath, "/") {
|
||||
return basePath + subId
|
||||
}
|
||||
return basePath + "/" + subId
|
||||
}
|
||||
|
||||
// BuildPageData parses header and prepares the template view model.
|
||||
// BuildPageData constructs page data for rendering the subscription information page.
|
||||
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string, basePath string) PageData {
|
||||
download := common.FormatTraffic(traffic.Down)
|
||||
upload := common.FormatTraffic(traffic.Up)
|
||||
total := "∞"
|
||||
used := common.FormatTraffic(traffic.Up + traffic.Down)
|
||||
remained := ""
|
||||
if traffic.Total > 0 {
|
||||
total = common.FormatTraffic(traffic.Total)
|
||||
left := max(traffic.Total-(traffic.Up+traffic.Down), 0)
|
||||
remained = common.FormatTraffic(left)
|
||||
}
|
||||
|
||||
datepicker := s.datepicker
|
||||
if datepicker == "" {
|
||||
datepicker = "gregorian"
|
||||
}
|
||||
|
||||
return PageData{
|
||||
Host: hostHeader,
|
||||
BasePath: basePath,
|
||||
SId: subId,
|
||||
Download: download,
|
||||
Upload: upload,
|
||||
Total: total,
|
||||
Used: used,
|
||||
Remained: remained,
|
||||
Expire: traffic.ExpiryTime / 1000,
|
||||
LastOnline: lastOnline,
|
||||
Datepicker: datepicker,
|
||||
DownloadByte: traffic.Down,
|
||||
UploadByte: traffic.Up,
|
||||
TotalByte: traffic.Total,
|
||||
SubUrl: subURL,
|
||||
SubJsonUrl: subJsonURL,
|
||||
Result: subs,
|
||||
}
|
||||
}
|
||||
|
||||
func getHostFromXFH(s string) (string, error) {
|
||||
if strings.Contains(s, ":") {
|
||||
realHost, _, err := net.SplitHostPort(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return realHost, nil
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
|
|
@ -1,26 +1,22 @@
|
|||
// Package common provides common utility functions for error handling, formatting, and multi-error management.
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"x-ui/logger"
|
||||
)
|
||||
|
||||
// NewErrorf creates a new error with formatted message.
|
||||
func NewErrorf(format string, a ...any) error {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// NewError creates a new error from the given arguments.
|
||||
func NewError(a ...any) error {
|
||||
msg := fmt.Sprintln(a...)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// Recover handles panic recovery and logs the panic error if a message is provided.
|
||||
func Recover(msg string) any {
|
||||
panicErr := recover()
|
||||
if panicErr != nil {
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"fmt"
|
||||
)
|
||||
|
||||
// FormatTraffic formats traffic bytes into human-readable units (B, KB, MB, GB, TB, PB).
|
||||
func FormatTraffic(trafficBytes int64) string {
|
||||
units := []string{"B", "KB", "MB", "GB", "TB", "PB"}
|
||||
unitIndex := 0
|
||||
|
|
|
@ -4,10 +4,8 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// multiError represents a collection of errors.
|
||||
type multiError []error
|
||||
|
||||
// Error returns a string representation of all errors joined with " | ".
|
||||
func (e multiError) Error() string {
|
||||
var r strings.Builder
|
||||
r.WriteString("multierr: ")
|
||||
|
@ -18,7 +16,6 @@ func (e multiError) Error() string {
|
|||
return r.String()
|
||||
}
|
||||
|
||||
// Combine combines multiple errors into a single error, filtering out nil errors.
|
||||
func Combine(maybeError ...error) error {
|
||||
var errs multiError
|
||||
for _, err := range maybeError {
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
// Package crypto provides cryptographic utilities for password hashing and verification.
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// HashPasswordAsBcrypt generates a bcrypt hash of the given password.
|
||||
func HashPasswordAsBcrypt(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(hash), err
|
||||
}
|
||||
|
||||
// CheckPasswordHash verifies if the given password matches the bcrypt hash.
|
||||
func CheckPasswordHash(hash, password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
// Package json_util provides JSON utilities including a custom RawMessage type.
|
||||
package json_util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// RawMessage is a custom JSON raw message type that marshals empty slices as "null".
|
||||
type RawMessage []byte
|
||||
|
||||
// MarshalJSON customizes the JSON marshaling behavior for RawMessage.
|
||||
// Empty RawMessage values are marshaled as "null" instead of "[]".
|
||||
// MarshalJSON: Customize json.RawMessage default behavior
|
||||
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||
if len(m) == 0 {
|
||||
return []byte("null"), nil
|
||||
|
@ -17,7 +14,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) {
|
|||
return m, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON sets *m to a copy of the JSON data.
|
||||
// UnmarshalJSON: sets *m to a copy of data.
|
||||
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
||||
if m == nil {
|
||||
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
|
||||
|
|
91
util/ldap/ldap.go
Normal file
91
util/ldap/ldap.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
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: true})
|
||||
} 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
|
||||
}
|
||||
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
// Package random provides utilities for generating random strings and numbers.
|
||||
package random
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -15,8 +13,6 @@ var (
|
|||
allSeq [62]rune
|
||||
)
|
||||
|
||||
// init initializes the character sequences used for random string generation.
|
||||
// It sets up arrays for numbers, lowercase letters, uppercase letters, and combinations.
|
||||
func init() {
|
||||
for i := 0; i < 10; i++ {
|
||||
numSeq[i] = rune('0' + i)
|
||||
|
@ -37,25 +33,14 @@ func init() {
|
|||
copy(allSeq[len(numSeq)+len(lowerSeq):], upperSeq[:])
|
||||
}
|
||||
|
||||
// Seq generates a random string of length n containing alphanumeric characters (numbers, lowercase and uppercase letters).
|
||||
func Seq(n int) string {
|
||||
runes := make([]rune, n)
|
||||
for i := 0; i < n; i++ {
|
||||
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()]
|
||||
runes[i] = allSeq[rand.Intn(len(allSeq))]
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
// Num generates a random integer between 0 and n-1.
|
||||
func Num(n int) int {
|
||||
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())
|
||||
return rand.Intn(n)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
// Package reflect_util provides reflection utilities for working with struct fields and values.
|
||||
package reflect_util
|
||||
|
||||
import "reflect"
|
||||
|
||||
// GetFields returns all struct fields of the given reflect.Type.
|
||||
func GetFields(t reflect.Type) []reflect.StructField {
|
||||
num := t.NumField()
|
||||
fields := make([]reflect.StructField, 0, num)
|
||||
|
@ -13,7 +11,6 @@ func GetFields(t reflect.Type) []reflect.StructField {
|
|||
return fields
|
||||
}
|
||||
|
||||
// GetFieldValues returns all field values of the given reflect.Value.
|
||||
func GetFieldValues(v reflect.Value) []reflect.Value {
|
||||
num := v.NumField()
|
||||
fields := make([]reflect.Value, 0, num)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// Package sys provides system utilities for monitoring network connections and CPU usage.
|
||||
// Platform-specific implementations are provided for Windows, Linux, and macOS.
|
||||
package sys
|
||||
|
||||
import (
|
||||
|
|
|
@ -4,12 +4,7 @@
|
|||
package sys
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func GetTCPCount() (int, error) {
|
||||
|
@ -27,69 +22,3 @@ func GetUDPCount() (int, error) {
|
|||
}
|
||||
return len(stats), nil
|
||||
}
|
||||
|
||||
// --- CPU Utilization (macOS native) ---
|
||||
|
||||
// sysctl kern.cp_time returns an array of 5 longs: user, nice, sys, idle, intr.
|
||||
// We compute utilization deltas without cgo.
|
||||
var (
|
||||
cpuMu sync.Mutex
|
||||
lastTotals [5]uint64
|
||||
hasLastCPUT bool
|
||||
)
|
||||
|
||||
func CPUPercentRaw() (float64, error) {
|
||||
raw, err := unix.SysctlRaw("kern.cp_time")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Expect either 5*8 bytes (uint64) or 5*4 bytes (uint32)
|
||||
var out [5]uint64
|
||||
switch len(raw) {
|
||||
case 5 * 8:
|
||||
for i := 0; i < 5; i++ {
|
||||
out[i] = binary.LittleEndian.Uint64(raw[i*8 : (i+1)*8])
|
||||
}
|
||||
case 5 * 4:
|
||||
for i := 0; i < 5; i++ {
|
||||
out[i] = uint64(binary.LittleEndian.Uint32(raw[i*4 : (i+1)*4]))
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("unexpected kern.cp_time size: %d", len(raw))
|
||||
}
|
||||
|
||||
// user, nice, sys, idle, intr
|
||||
user := out[0]
|
||||
nice := out[1]
|
||||
sysv := out[2]
|
||||
idle := out[3]
|
||||
intr := out[4]
|
||||
|
||||
cpuMu.Lock()
|
||||
defer cpuMu.Unlock()
|
||||
|
||||
if !hasLastCPUT {
|
||||
lastTotals = out
|
||||
hasLastCPUT = true
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
dUser := user - lastTotals[0]
|
||||
dNice := nice - lastTotals[1]
|
||||
dSys := sysv - lastTotals[2]
|
||||
dIdle := idle - lastTotals[3]
|
||||
dIntr := intr - lastTotals[4]
|
||||
|
||||
lastTotals = out
|
||||
|
||||
totald := dUser + dNice + dSys + dIdle + dIntr
|
||||
if totald == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
busy := totald - dIdle
|
||||
pct := float64(busy) / float64(totald) * 100.0
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
return pct, nil
|
||||
}
|
||||
|
|
|
@ -4,14 +4,10 @@
|
|||
package sys
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func getLinesNum(filename string) (int, error) {
|
||||
|
@ -45,8 +41,6 @@ func getLinesNum(filename string) (int, error) {
|
|||
return sum, nil
|
||||
}
|
||||
|
||||
// GetTCPCount returns the number of active TCP connections by reading
|
||||
// /proc/net/tcp and /proc/net/tcp6 when available.
|
||||
func GetTCPCount() (int, error) {
|
||||
root := HostProc()
|
||||
|
||||
|
@ -77,8 +71,6 @@ func GetUDPCount() (int, error) {
|
|||
return udp4 + udp6, nil
|
||||
}
|
||||
|
||||
// safeGetLinesNum returns 0 if the file does not exist, otherwise forwards
|
||||
// to getLinesNum to count the number of lines.
|
||||
func safeGetLinesNum(path string) (int, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
|
@ -87,99 +79,3 @@ func safeGetLinesNum(path string) (int, error) {
|
|||
}
|
||||
return getLinesNum(path)
|
||||
}
|
||||
|
||||
// --- CPU Utilization (Linux native) ---
|
||||
|
||||
var (
|
||||
cpuMu sync.Mutex
|
||||
lastTotal uint64
|
||||
lastIdleAll uint64
|
||||
hasLast bool
|
||||
)
|
||||
|
||||
// CPUPercentRaw returns instantaneous total CPU utilization by reading /proc/stat.
|
||||
// First call initializes and returns 0; subsequent calls return busy/total * 100.
|
||||
func CPUPercentRaw() (float64, error) {
|
||||
f, err := os.Open("/proc/stat")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
rd := bufio.NewReader(f)
|
||||
line, err := rd.ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return 0, err
|
||||
}
|
||||
// Expect line like: cpu user nice system idle iowait irq softirq steal guest guest_nice
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 5 || fields[0] != "cpu" {
|
||||
return 0, fmt.Errorf("unexpected /proc/stat format")
|
||||
}
|
||||
|
||||
var nums []uint64
|
||||
for i := 1; i < len(fields); i++ {
|
||||
v, err := strconv.ParseUint(fields[i], 10, 64)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
nums = append(nums, v)
|
||||
}
|
||||
if len(nums) < 4 { // need at least user,nice,system,idle
|
||||
return 0, fmt.Errorf("insufficient cpu fields")
|
||||
}
|
||||
|
||||
// Conform with standard Linux CPU accounting
|
||||
var user, nice, system, idle, iowait, irq, softirq, steal uint64
|
||||
user = nums[0]
|
||||
if len(nums) > 1 {
|
||||
nice = nums[1]
|
||||
}
|
||||
if len(nums) > 2 {
|
||||
system = nums[2]
|
||||
}
|
||||
if len(nums) > 3 {
|
||||
idle = nums[3]
|
||||
}
|
||||
if len(nums) > 4 {
|
||||
iowait = nums[4]
|
||||
}
|
||||
if len(nums) > 5 {
|
||||
irq = nums[5]
|
||||
}
|
||||
if len(nums) > 6 {
|
||||
softirq = nums[6]
|
||||
}
|
||||
if len(nums) > 7 {
|
||||
steal = nums[7]
|
||||
}
|
||||
|
||||
idleAll := idle + iowait
|
||||
nonIdle := user + nice + system + irq + softirq + steal
|
||||
total := idleAll + nonIdle
|
||||
|
||||
cpuMu.Lock()
|
||||
defer cpuMu.Unlock()
|
||||
|
||||
if !hasLast {
|
||||
lastTotal = total
|
||||
lastIdleAll = idleAll
|
||||
hasLast = true
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
totald := total - lastTotal
|
||||
idled := idleAll - lastIdleAll
|
||||
lastTotal = total
|
||||
lastIdleAll = idleAll
|
||||
|
||||
if totald == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
busy := totald - idled
|
||||
pct := float64(busy) / float64(totald) * 100.0
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
return pct, nil
|
||||
}
|
||||
|
|
|
@ -5,14 +5,10 @@ package sys
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
// GetConnectionCount returns the number of active connections for the specified protocol ("tcp" or "udp").
|
||||
func GetConnectionCount(proto string) (int, error) {
|
||||
if proto != "tcp" && proto != "udp" {
|
||||
return 0, errors.New("invalid protocol")
|
||||
|
@ -25,92 +21,10 @@ func GetConnectionCount(proto string) (int, error) {
|
|||
return len(stats), nil
|
||||
}
|
||||
|
||||
// GetTCPCount returns the number of active TCP connections.
|
||||
func GetTCPCount() (int, error) {
|
||||
return GetConnectionCount("tcp")
|
||||
}
|
||||
|
||||
// GetUDPCount returns the number of active UDP connections.
|
||||
func GetUDPCount() (int, error) {
|
||||
return GetConnectionCount("udp")
|
||||
}
|
||||
|
||||
// --- CPU Utilization (Windows native) ---
|
||||
|
||||
var (
|
||||
modKernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procGetSystemTimes = modKernel32.NewProc("GetSystemTimes")
|
||||
|
||||
cpuMu sync.Mutex
|
||||
lastIdle uint64
|
||||
lastKernel uint64
|
||||
lastUser uint64
|
||||
hasLast bool
|
||||
)
|
||||
|
||||
type filetime struct {
|
||||
LowDateTime uint32
|
||||
HighDateTime uint32
|
||||
}
|
||||
|
||||
// ftToUint64 converts a Windows FILETIME-like struct to a uint64 for
|
||||
// arithmetic and delta calculations used by CPUPercentRaw.
|
||||
func ftToUint64(ft filetime) uint64 {
|
||||
return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime)
|
||||
}
|
||||
|
||||
// CPUPercentRaw returns the instantaneous total CPU utilization percentage using
|
||||
// Windows GetSystemTimes across all logical processors. The first call returns 0
|
||||
// as it initializes the baseline. Subsequent calls compute deltas.
|
||||
func CPUPercentRaw() (float64, error) {
|
||||
var idleFT, kernelFT, userFT filetime
|
||||
r1, _, e1 := procGetSystemTimes.Call(
|
||||
uintptr(unsafe.Pointer(&idleFT)),
|
||||
uintptr(unsafe.Pointer(&kernelFT)),
|
||||
uintptr(unsafe.Pointer(&userFT)),
|
||||
)
|
||||
if r1 == 0 { // failure
|
||||
if e1 != nil {
|
||||
return 0, e1
|
||||
}
|
||||
return 0, syscall.GetLastError()
|
||||
}
|
||||
|
||||
idle := ftToUint64(idleFT)
|
||||
kernel := ftToUint64(kernelFT)
|
||||
user := ftToUint64(userFT)
|
||||
|
||||
cpuMu.Lock()
|
||||
defer cpuMu.Unlock()
|
||||
|
||||
if !hasLast {
|
||||
lastIdle = idle
|
||||
lastKernel = kernel
|
||||
lastUser = user
|
||||
hasLast = true
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
idleDelta := idle - lastIdle
|
||||
kernelDelta := kernel - lastKernel
|
||||
userDelta := user - lastUser
|
||||
|
||||
// Update for next call
|
||||
lastIdle = idle
|
||||
lastKernel = kernel
|
||||
lastUser = user
|
||||
|
||||
total := kernelDelta + userDelta
|
||||
if total == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
// On Windows, kernel time includes idle time; busy = total - idle
|
||||
busy := total - idleDelta
|
||||
|
||||
pct := float64(busy) / float64(total) * 100.0
|
||||
// lower bound not needed; ratios of uint64 are non-negative
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
return pct, nil
|
||||
}
|
||||
|
|
3
web/assets/ant-design-vue/antd-with-locales.min.js
vendored
Normal file
3
web/assets/ant-design-vue/antd-with-locales.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
web/assets/ant-design-vue/antd.less
Normal file
7
web/assets/ant-design-vue/antd.less
Normal file
|
@ -0,0 +1,7 @@
|
|||
@import "../lib/style/index.less";
|
||||
@import "../lib/style/components.less";
|
||||
|
||||
@green-6: #008771;
|
||||
@primary-color: @green-6;
|
||||
@border-radius-base: 1rem;
|
||||
@progress-remaining-color: #EDEDED;
|
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
2
web/assets/css/custom.min.css
vendored
2
web/assets/css/custom.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -6,12 +6,9 @@ class DBInbound {
|
|||
this.up = 0;
|
||||
this.down = 0;
|
||||
this.total = 0;
|
||||
this.allTime = 0;
|
||||
this.remark = "";
|
||||
this.enable = true;
|
||||
this.expiryTime = 0;
|
||||
this.trafficReset = "never";
|
||||
this.lastTrafficResetTime = 0;
|
||||
|
||||
this.listen = "";
|
||||
this.port = 0;
|
||||
|
@ -51,8 +48,8 @@ class DBInbound {
|
|||
return this.protocol === Protocols.SHADOWSOCKS;
|
||||
}
|
||||
|
||||
get isMixed() {
|
||||
return this.protocol === Protocols.MIXED;
|
||||
get isSocks() {
|
||||
return this.protocol === Protocols.SOCKS;
|
||||
}
|
||||
|
||||
get isHTTP() {
|
||||
|
|
|
@ -3,16 +3,18 @@ const Protocols = {
|
|||
VLESS: 'vless',
|
||||
TROJAN: 'trojan',
|
||||
SHADOWSOCKS: 'shadowsocks',
|
||||
TUNNEL: 'tunnel',
|
||||
MIXED: 'mixed',
|
||||
DOKODEMO: 'dokodemo-door',
|
||||
SOCKS: 'socks',
|
||||
HTTP: 'http',
|
||||
WIREGUARD: 'wireguard',
|
||||
};
|
||||
|
||||
const SSMethods = {
|
||||
AES_256_GCM: 'aes-256-gcm',
|
||||
AES_128_GCM: 'aes-128-gcm',
|
||||
CHACHA20_POLY1305: 'chacha20-poly1305',
|
||||
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
|
||||
XCHACHA20_POLY1305: 'xchacha20-poly1305',
|
||||
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
|
||||
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
|
||||
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
|
||||
|
@ -639,6 +641,7 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
|||
keyFile = '',
|
||||
certificate = '',
|
||||
key = '',
|
||||
ocspStapling = 0,
|
||||
oneTimeLoading = false,
|
||||
usage = USAGE_OPTION.ENCIPHERMENT,
|
||||
buildChain = false,
|
||||
|
@ -649,6 +652,7 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
|||
this.keyFile = keyFile;
|
||||
this.cert = Array.isArray(certificate) ? certificate.join('\n') : certificate;
|
||||
this.key = Array.isArray(key) ? key.join('\n') : key;
|
||||
this.ocspStapling = ocspStapling;
|
||||
this.oneTimeLoading = oneTimeLoading;
|
||||
this.usage = usage;
|
||||
this.buildChain = buildChain
|
||||
|
@ -660,6 +664,7 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
|||
true,
|
||||
json.certificateFile,
|
||||
json.keyFile, '', '',
|
||||
json.ocspStapling,
|
||||
json.oneTimeLoading,
|
||||
json.usage,
|
||||
json.buildChain,
|
||||
|
@ -669,6 +674,7 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
|||
false, '', '',
|
||||
json.certificate.join('\n'),
|
||||
json.key.join('\n'),
|
||||
json.ocspStapling,
|
||||
json.oneTimeLoading,
|
||||
json.usage,
|
||||
json.buildChain,
|
||||
|
@ -681,6 +687,7 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
|||
return {
|
||||
certificateFile: this.certFile,
|
||||
keyFile: this.keyFile,
|
||||
ocspStapling: this.ocspStapling,
|
||||
oneTimeLoading: this.oneTimeLoading,
|
||||
usage: this.usage,
|
||||
buildChain: this.buildChain,
|
||||
|
@ -689,6 +696,7 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
|||
return {
|
||||
certificate: this.cert.split('\n'),
|
||||
key: this.key.split('\n'),
|
||||
ocspStapling: this.ocspStapling,
|
||||
oneTimeLoading: this.oneTimeLoading,
|
||||
usage: this.usage,
|
||||
buildChain: this.buildChain,
|
||||
|
@ -729,7 +737,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
|||
constructor(
|
||||
show = false,
|
||||
xver = 0,
|
||||
target = 'google.com:443',
|
||||
dest = 'google.com:443',
|
||||
serverNames = 'google.com,www.google.com',
|
||||
privateKey = '',
|
||||
minClientVer = '',
|
||||
|
@ -742,7 +750,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
|||
super();
|
||||
this.show = show;
|
||||
this.xver = xver;
|
||||
this.target = target;
|
||||
this.dest = dest;
|
||||
this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames;
|
||||
this.privateKey = privateKey;
|
||||
this.minClientVer = minClientVer;
|
||||
|
@ -767,7 +775,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
|||
return new RealityStreamSettings(
|
||||
json.show,
|
||||
json.xver,
|
||||
json.target,
|
||||
json.dest,
|
||||
json.serverNames,
|
||||
json.privateKey,
|
||||
json.minClientVer,
|
||||
|
@ -783,7 +791,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
|||
return {
|
||||
show: this.show,
|
||||
xver: this.xver,
|
||||
target: this.target,
|
||||
dest: this.dest,
|
||||
serverNames: this.serverNames.split(","),
|
||||
privateKey: this.privateKey,
|
||||
minClientVer: this.minClientVer,
|
||||
|
@ -1040,6 +1048,27 @@ class Sniffing extends XrayCommonClass {
|
|||
}
|
||||
}
|
||||
|
||||
class Allocate extends XrayCommonClass {
|
||||
constructor(
|
||||
strategy = "always",
|
||||
refresh = 5,
|
||||
concurrency = 3,
|
||||
) {
|
||||
super();
|
||||
this.strategy = strategy;
|
||||
this.refresh = refresh;
|
||||
this.concurrency = concurrency;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new Allocate(
|
||||
json.strategy,
|
||||
json.refresh,
|
||||
json.concurrency,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Inbound extends XrayCommonClass {
|
||||
constructor(
|
||||
port = RandomUtil.randomInteger(10000, 60000),
|
||||
|
@ -1049,6 +1078,7 @@ class Inbound extends XrayCommonClass {
|
|||
streamSettings = new StreamSettings(),
|
||||
tag = '',
|
||||
sniffing = new Sniffing(),
|
||||
allocate = new Allocate(),
|
||||
clientStats = '',
|
||||
) {
|
||||
super();
|
||||
|
@ -1059,6 +1089,7 @@ class Inbound extends XrayCommonClass {
|
|||
this.stream = streamSettings;
|
||||
this.tag = tag;
|
||||
this.sniffing = sniffing;
|
||||
this.allocate = allocate;
|
||||
this.clientStats = clientStats;
|
||||
}
|
||||
getClientStats() {
|
||||
|
@ -1223,6 +1254,7 @@ class Inbound extends XrayCommonClass {
|
|||
this.stream = new StreamSettings();
|
||||
this.tag = '';
|
||||
this.sniffing = new Sniffing();
|
||||
this.allocate = new Allocate();
|
||||
}
|
||||
|
||||
genVmessLink(address = '', port = this.port, forceTls, remark = '', clientId, security) {
|
||||
|
@ -1299,7 +1331,6 @@ class Inbound extends XrayCommonClass {
|
|||
const security = forceTls == 'same' ? this.stream.security : forceTls;
|
||||
const params = new Map();
|
||||
params.set("type", this.stream.network);
|
||||
params.set("encryption", this.settings.encryption);
|
||||
switch (type) {
|
||||
case "tcp":
|
||||
const tcp = this.stream.tcp;
|
||||
|
@ -1678,13 +1709,14 @@ class Inbound extends XrayCommonClass {
|
|||
StreamSettings.fromJson(json.streamSettings),
|
||||
json.tag,
|
||||
Sniffing.fromJson(json.sniffing),
|
||||
Allocate.fromJson(json.allocate),
|
||||
json.clientStats
|
||||
)
|
||||
}
|
||||
|
||||
toJson() {
|
||||
let streamSettings;
|
||||
if (this.canEnableStream() || this.stream?.sockopt) {
|
||||
if (this.canEnableStream()) {
|
||||
streamSettings = this.stream.toJson();
|
||||
}
|
||||
return {
|
||||
|
@ -1695,6 +1727,7 @@ class Inbound extends XrayCommonClass {
|
|||
streamSettings: streamSettings,
|
||||
tag: this.tag,
|
||||
sniffing: this.sniffing.toJson(),
|
||||
allocate: this.allocate.toJson(),
|
||||
clientStats: this.clientStats
|
||||
};
|
||||
}
|
||||
|
@ -1712,8 +1745,8 @@ Inbound.Settings = class extends XrayCommonClass {
|
|||
case Protocols.VLESS: return new Inbound.VLESSSettings(protocol);
|
||||
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
|
||||
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
|
||||
case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol);
|
||||
case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
|
||||
case Protocols.DOKODEMO: return new Inbound.DokodemoSettings(protocol);
|
||||
case Protocols.SOCKS: return new Inbound.SocksSettings(protocol);
|
||||
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
|
||||
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
|
||||
default: return null;
|
||||
|
@ -1726,8 +1759,8 @@ Inbound.Settings = class extends XrayCommonClass {
|
|||
case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json);
|
||||
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
|
||||
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
|
||||
case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json);
|
||||
case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
|
||||
case Protocols.DOKODEMO: return Inbound.DokodemoSettings.fromJson(json);
|
||||
case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json);
|
||||
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
|
||||
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
|
||||
default: return null;
|
||||
|
@ -1790,9 +1823,7 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
|||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
reset = 0
|
||||
) {
|
||||
super();
|
||||
this.id = id;
|
||||
|
@ -1806,8 +1837,6 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
|||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
|
@ -1823,8 +1852,6 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
|||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
get _expiryTime() {
|
||||
|
@ -1858,17 +1885,13 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||
constructor(
|
||||
protocol,
|
||||
vlesses = [new Inbound.VLESSSettings.VLESS()],
|
||||
decryption = "none",
|
||||
encryption = "none",
|
||||
fallbacks = [],
|
||||
selectedAuth = undefined,
|
||||
decryption = 'none',
|
||||
fallbacks = []
|
||||
) {
|
||||
super(protocol);
|
||||
this.vlesses = vlesses;
|
||||
this.decryption = decryption;
|
||||
this.encryption = encryption;
|
||||
this.fallbacks = fallbacks;
|
||||
this.selectedAuth = selectedAuth;
|
||||
}
|
||||
|
||||
addFallback() {
|
||||
|
@ -1879,43 +1902,22 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||
this.fallbacks.splice(index, 1);
|
||||
}
|
||||
|
||||
// decryption should be set to static value
|
||||
static fromJson(json = {}) {
|
||||
const obj = new Inbound.VLESSSettings(
|
||||
return new Inbound.VLESSSettings(
|
||||
Protocols.VLESS,
|
||||
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
|
||||
json.decryption,
|
||||
json.encryption,
|
||||
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
|
||||
json.selectedAuth
|
||||
);
|
||||
return obj;
|
||||
json.clients.map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
|
||||
json.decryption || 'none',
|
||||
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks),);
|
||||
}
|
||||
|
||||
|
||||
toJson() {
|
||||
const json = {
|
||||
return {
|
||||
clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
|
||||
decryption: this.decryption,
|
||||
fallbacks: Inbound.VLESSSettings.toJsonArray(this.fallbacks),
|
||||
};
|
||||
|
||||
if (this.decryption) {
|
||||
json.decryption = this.decryption;
|
||||
}
|
||||
|
||||
if (this.encryption) {
|
||||
json.encryption = this.encryption;
|
||||
}
|
||||
|
||||
if (this.fallbacks && this.fallbacks.length > 0) {
|
||||
json.fallbacks = Inbound.VLESSSettings.toJsonArray(this.fallbacks);
|
||||
}
|
||||
if (this.selectedAuth) {
|
||||
json.selectedAuth = this.selectedAuth;
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
|
@ -1930,9 +1932,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
|||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
reset = 0
|
||||
) {
|
||||
super();
|
||||
this.id = id;
|
||||
|
@ -1946,8 +1946,6 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
|||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
|
@ -1963,8 +1961,6 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
|||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2075,9 +2071,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
reset = 0
|
||||
) {
|
||||
super();
|
||||
this.password = password;
|
||||
|
@ -2090,8 +2084,6 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
|
@ -2106,8 +2098,6 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||
subId: this.subId,
|
||||
comment: this.comment,
|
||||
reset: this.reset,
|
||||
created_at: this.created_at,
|
||||
updated_at: this.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2123,8 +2113,6 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2244,9 +2232,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
|||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
reset = 0
|
||||
) {
|
||||
super();
|
||||
this.method = method;
|
||||
|
@ -2260,8 +2246,6 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
|||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
|
@ -2277,8 +2261,6 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
|||
subId: this.subId,
|
||||
comment: this.comment,
|
||||
reset: this.reset,
|
||||
created_at: this.created_at,
|
||||
updated_at: this.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2295,8 +2277,6 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
|||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2327,7 +2307,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
|||
|
||||
};
|
||||
|
||||
Inbound.TunnelSettings = class extends Inbound.Settings {
|
||||
Inbound.DokodemoSettings = class extends Inbound.Settings {
|
||||
constructor(
|
||||
protocol,
|
||||
address,
|
||||
|
@ -2345,8 +2325,8 @@ Inbound.TunnelSettings = class extends Inbound.Settings {
|
|||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new Inbound.TunnelSettings(
|
||||
Protocols.TUNNEL,
|
||||
return new Inbound.DokodemoSettings(
|
||||
Protocols.DOKODEMO,
|
||||
json.address,
|
||||
json.port,
|
||||
XrayCommonClass.toHeaders(json.portMap),
|
||||
|
@ -2366,8 +2346,8 @@ Inbound.TunnelSettings = class extends Inbound.Settings {
|
|||
}
|
||||
};
|
||||
|
||||
Inbound.MixedSettings = class extends Inbound.Settings {
|
||||
constructor(protocol, auth = 'password', accounts = [new Inbound.MixedSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
|
||||
Inbound.SocksSettings = class extends Inbound.Settings {
|
||||
constructor(protocol, auth = 'password', accounts = [new Inbound.SocksSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
|
||||
super(protocol);
|
||||
this.auth = auth;
|
||||
this.accounts = accounts;
|
||||
|
@ -2387,11 +2367,11 @@ Inbound.MixedSettings = class extends Inbound.Settings {
|
|||
let accounts;
|
||||
if (json.auth === 'password') {
|
||||
accounts = json.accounts.map(
|
||||
account => Inbound.MixedSettings.SocksAccount.fromJson(account)
|
||||
account => Inbound.SocksSettings.SocksAccount.fromJson(account)
|
||||
)
|
||||
}
|
||||
return new Inbound.MixedSettings(
|
||||
Protocols.MIXED,
|
||||
return new Inbound.SocksSettings(
|
||||
Protocols.SOCKS,
|
||||
json.auth,
|
||||
accounts,
|
||||
json.udp,
|
||||
|
@ -2408,7 +2388,7 @@ Inbound.MixedSettings = class extends Inbound.Settings {
|
|||
};
|
||||
}
|
||||
};
|
||||
Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
|
||||
Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass {
|
||||
constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
|
||||
super();
|
||||
this.user = user;
|
||||
|
@ -2416,7 +2396,7 @@ Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
|
|||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new Inbound.MixedSettings.SocksAccount(json.user, json.pass);
|
||||
return new Inbound.SocksSettings.SocksAccount(json.user, json.pass);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -219,7 +219,7 @@ class KcpStreamSettings extends CommonClass {
|
|||
|
||||
class WsStreamSettings extends CommonClass {
|
||||
constructor(
|
||||
path = '/',
|
||||
path = '/',
|
||||
host = '',
|
||||
heartbeatPeriod = 0,
|
||||
|
||||
|
@ -647,6 +647,10 @@ class Outbound extends CommonClass {
|
|||
].includes(this.protocol);
|
||||
}
|
||||
|
||||
hasVnext() {
|
||||
return [Protocols.VMess, Protocols.VLESS].includes(this.protocol);
|
||||
}
|
||||
|
||||
hasServers() {
|
||||
return [Protocols.Trojan, Protocols.Shadowsocks, Protocols.Socks, Protocols.HTTP].includes(this.protocol);
|
||||
}
|
||||
|
@ -686,15 +690,13 @@ class Outbound extends CommonClass {
|
|||
if (this.stream?.sockopt)
|
||||
stream = { sockopt: this.stream.sockopt.toJson() };
|
||||
}
|
||||
let settingsOut = this.settings instanceof CommonClass ? this.settings.toJson() : this.settings;
|
||||
return {
|
||||
tag: this.tag == '' ? undefined : this.tag,
|
||||
protocol: this.protocol,
|
||||
settings: settingsOut,
|
||||
// Only include tag, streamSettings, sendThrough, mux if present and not empty
|
||||
...(this.tag ? { tag: this.tag } : {}),
|
||||
...(stream ? { streamSettings: stream } : {}),
|
||||
...(this.sendThrough ? { sendThrough: this.sendThrough } : {}),
|
||||
...(this.mux?.enabled ? { mux: this.mux } : {}),
|
||||
settings: this.settings instanceof CommonClass ? this.settings.toJson() : this.settings,
|
||||
streamSettings: stream,
|
||||
sendThrough: this.sendThrough != "" ? this.sendThrough : undefined,
|
||||
mux: this.mux?.enabled ? this.mux : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -811,7 +813,7 @@ class Outbound extends CommonClass {
|
|||
var settings;
|
||||
switch (protocol) {
|
||||
case Protocols.VLESS:
|
||||
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '', url.searchParams.get('encryption') ?? 'none');
|
||||
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '');
|
||||
break;
|
||||
case Protocols.Trojan:
|
||||
settings = new Outbound.TrojanSettings(address, port, userData);
|
||||
|
@ -906,7 +908,7 @@ Outbound.FreedomSettings = class extends CommonClass {
|
|||
toJson() {
|
||||
return {
|
||||
domainStrategy: ObjectUtil.isEmpty(this.domainStrategy) ? undefined : this.domainStrategy,
|
||||
redirect: ObjectUtil.isEmpty(this.redirect) ? undefined : this.redirect,
|
||||
redirect: ObjectUtil.isEmpty(this.redirect) ? undefined: this.redirect,
|
||||
fragment: Object.keys(this.fragment).length === 0 ? undefined : this.fragment,
|
||||
noises: this.noises.length === 0 ? undefined : Outbound.FreedomSettings.Noise.toJsonArray(this.noises),
|
||||
};
|
||||
|
@ -917,14 +919,12 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
|
|||
constructor(
|
||||
packets = '1-3',
|
||||
length = '',
|
||||
interval = '',
|
||||
maxSplit = ''
|
||||
interval = ''
|
||||
) {
|
||||
super();
|
||||
this.packets = packets;
|
||||
this.length = length;
|
||||
this.interval = interval;
|
||||
this.maxSplit = maxSplit;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
|
@ -932,7 +932,6 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
|
|||
json.packets,
|
||||
json.length,
|
||||
json.interval,
|
||||
json.maxSplit
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -941,14 +940,12 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
|
|||
constructor(
|
||||
type = 'rand',
|
||||
packet = '10-20',
|
||||
delay = '10-16',
|
||||
applyTo = 'ip'
|
||||
delay = '10-16'
|
||||
) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.packet = packet;
|
||||
this.delay = delay;
|
||||
this.applyTo = applyTo;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
|
@ -956,7 +953,6 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
|
|||
json.type,
|
||||
json.packet,
|
||||
json.delay,
|
||||
json.applyTo
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -965,7 +961,6 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
|
|||
type: this.type,
|
||||
packet: this.packet,
|
||||
delay: this.delay,
|
||||
applyTo: this.applyTo
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -993,7 +988,7 @@ Outbound.DNSSettings = class extends CommonClass {
|
|||
network = 'udp',
|
||||
address = '',
|
||||
port = 53,
|
||||
nonIPQuery = 'reject',
|
||||
nonIPQuery = 'drop',
|
||||
blockTypes = []
|
||||
) {
|
||||
super();
|
||||
|
@ -1024,16 +1019,13 @@ Outbound.VmessSettings = class extends CommonClass {
|
|||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
if (!ObjectUtil.isArrEmpty(json.vnext)) {
|
||||
const v = json.vnext[0] || {};
|
||||
const u = ObjectUtil.isArrEmpty(v.users) ? {} : v.users[0];
|
||||
return new Outbound.VmessSettings(
|
||||
v.address,
|
||||
v.port,
|
||||
u.id,
|
||||
u.security,
|
||||
);
|
||||
}
|
||||
if (ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VmessSettings();
|
||||
return new Outbound.VmessSettings(
|
||||
json.vnext[0].address,
|
||||
json.vnext[0].port,
|
||||
json.vnext[0].users[0].id,
|
||||
json.vnext[0].users[0].security,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
|
@ -1041,42 +1033,39 @@ Outbound.VmessSettings = class extends CommonClass {
|
|||
vnext: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
users: [{
|
||||
id: this.id,
|
||||
security: this.security
|
||||
}]
|
||||
}]
|
||||
users: [{ id: this.id, security: this.security }],
|
||||
}],
|
||||
};
|
||||
}
|
||||
};
|
||||
Outbound.VLESSSettings = class extends CommonClass {
|
||||
constructor(address, port, id, flow, encryption) {
|
||||
constructor(address, port, id, flow, encryption = 'none') {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.id = id;
|
||||
this.flow = flow;
|
||||
this.encryption = encryption;
|
||||
this.encryption = encryption
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VLESSSettings();
|
||||
if (ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VLESSSettings();
|
||||
return new Outbound.VLESSSettings(
|
||||
json.address,
|
||||
json.port,
|
||||
json.id,
|
||||
json.flow,
|
||||
json.encryption
|
||||
json.vnext[0].address,
|
||||
json.vnext[0].port,
|
||||
json.vnext[0].users[0].id,
|
||||
json.vnext[0].users[0].flow,
|
||||
json.vnext[0].users[0].encryption,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
id: this.id,
|
||||
flow: this.flow,
|
||||
encryption: this.encryption,
|
||||
vnext: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
users: [{ id: this.id, flow: this.flow, encryption: 'none', }],
|
||||
}],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@ class AllSetting {
|
|||
this.webKeyFile = "";
|
||||
this.webBasePath = "/";
|
||||
this.sessionMaxAge = 360;
|
||||
this.pageSize = 25;
|
||||
this.pageSize = 50;
|
||||
this.expireDiff = 0;
|
||||
this.trafficDiff = 0;
|
||||
this.remarkModel = "-ieo";
|
||||
|
@ -26,8 +26,7 @@ class AllSetting {
|
|||
this.twoFactorEnable = false;
|
||||
this.twoFactorToken = "";
|
||||
this.xrayTemplateConfig = "";
|
||||
this.subEnable = true;
|
||||
this.subJsonEnable = false;
|
||||
this.subEnable = false;
|
||||
this.subTitle = "";
|
||||
this.subListen = "";
|
||||
this.subPort = 2096;
|
||||
|
@ -50,6 +49,28 @@ class AllSetting {
|
|||
|
||||
this.timeLocation = "Local";
|
||||
|
||||
// LDAP settings
|
||||
this.ldapEnable = false;
|
||||
this.ldapHost = "";
|
||||
this.ldapPort = 389;
|
||||
this.ldapUseTLS = false;
|
||||
this.ldapBindDN = "";
|
||||
this.ldapPassword = "";
|
||||
this.ldapBaseDN = "";
|
||||
this.ldapUserFilter = "(objectClass=person)";
|
||||
this.ldapUserAttr = "mail";
|
||||
this.ldapVlessField = "vless_enabled";
|
||||
this.ldapSyncCron = "@every 1m";
|
||||
this.ldapFlagField = "";
|
||||
this.ldapTruthyValues = "true,1,yes,on";
|
||||
this.ldapInvertFlag = false;
|
||||
this.ldapInboundTags = "";
|
||||
this.ldapAutoCreate = false;
|
||||
this.ldapAutoDelete = false;
|
||||
this.ldapDefaultTotalGB = 0;
|
||||
this.ldapDefaultExpiryDays = 0;
|
||||
this.ldapDefaultLimitIP = 0;
|
||||
|
||||
if (data == null) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -134,7 +134,7 @@ class DateUtil {
|
|||
}
|
||||
|
||||
static formatMillis(millis) {
|
||||
return moment(millis).format('YYYY-M-D HH:mm:ss');
|
||||
return moment(millis).format('YYYY-M-D H:m:s');
|
||||
}
|
||||
|
||||
static firstDayOfMonth() {
|
||||
|
|
|
@ -316,23 +316,13 @@ class ObjectUtil {
|
|||
}
|
||||
|
||||
static equals(a, b) {
|
||||
for (const key in a) {
|
||||
if (!a.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
if (!b.hasOwnProperty(key)) {
|
||||
return false;
|
||||
} else if (a[key] !== b[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const key in b) {
|
||||
if (!b.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
if (!a.hasOwnProperty(key)) {
|
||||
return false;
|
||||
}
|
||||
// shallow, symmetric comparison so newly added fields also affect equality
|
||||
const aKeys = Object.keys(a);
|
||||
const bKeys = Object.keys(b);
|
||||
if (aKeys.length !== bKeys.length) return false;
|
||||
for (const key of aKeys) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
||||
if (a[key] !== b[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
11818
web/assets/vue/vue.common.dev.js
Normal file
11818
web/assets/vue/vue.common.dev.js
Normal file
File diff suppressed because it is too large
Load diff
5
web/assets/vue/vue.common.js
Normal file
5
web/assets/vue/vue.common.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./vue.common.prod.js')
|
||||
} else {
|
||||
module.exports = require('./vue.common.dev.js')
|
||||
}
|
6
web/assets/vue/vue.common.prod.js
Normal file
6
web/assets/vue/vue.common.prod.js
Normal file
File diff suppressed because one or more lines are too long
11731
web/assets/vue/vue.esm.browser.js
Normal file
11731
web/assets/vue/vue.esm.browser.js
Normal file
File diff suppressed because it is too large
Load diff
6
web/assets/vue/vue.esm.browser.min.js
vendored
Normal file
6
web/assets/vue/vue.esm.browser.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
11918
web/assets/vue/vue.esm.js
Normal file
11918
web/assets/vue/vue.esm.js
Normal file
File diff suppressed because it is too large
Load diff
11932
web/assets/vue/vue.js
Normal file
11932
web/assets/vue/vue.js
Normal file
File diff suppressed because it is too large
Load diff
8736
web/assets/vue/vue.runtime.common.dev.js
Normal file
8736
web/assets/vue/vue.runtime.common.dev.js
Normal file
File diff suppressed because it is too large
Load diff
5
web/assets/vue/vue.runtime.common.js
Normal file
5
web/assets/vue/vue.runtime.common.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./vue.runtime.common.prod.js')
|
||||
} else {
|
||||
module.exports = require('./vue.runtime.common.dev.js')
|
||||
}
|
6
web/assets/vue/vue.runtime.common.prod.js
Normal file
6
web/assets/vue/vue.runtime.common.prod.js
Normal file
File diff suppressed because one or more lines are too long
8825
web/assets/vue/vue.runtime.esm.js
Normal file
8825
web/assets/vue/vue.runtime.esm.js
Normal file
File diff suppressed because it is too large
Load diff
8826
web/assets/vue/vue.runtime.js
Normal file
8826
web/assets/vue/vue.runtime.js
Normal file
File diff suppressed because it is too large
Load diff
6
web/assets/vue/vue.runtime.min.js
vendored
Normal file
6
web/assets/vue/vue.runtime.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
76
web/assets/vue/vue.runtime.mjs
Normal file
76
web/assets/vue/vue.runtime.mjs
Normal file
|
@ -0,0 +1,76 @@
|
|||
import Vue from './vue.runtime.common.js'
|
||||
export default Vue
|
||||
|
||||
// this should be kept in sync with src/v3/index.ts
|
||||
export const {
|
||||
version,
|
||||
|
||||
// refs
|
||||
ref,
|
||||
shallowRef,
|
||||
isRef,
|
||||
toRef,
|
||||
toRefs,
|
||||
unref,
|
||||
proxyRefs,
|
||||
customRef,
|
||||
triggerRef,
|
||||
computed,
|
||||
|
||||
// reactive
|
||||
reactive,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isShallow,
|
||||
isProxy,
|
||||
shallowReactive,
|
||||
markRaw,
|
||||
toRaw,
|
||||
readonly,
|
||||
shallowReadonly,
|
||||
|
||||
// watch
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
|
||||
// effectScope
|
||||
effectScope,
|
||||
onScopeDispose,
|
||||
getCurrentScope,
|
||||
|
||||
// provide / inject
|
||||
provide,
|
||||
inject,
|
||||
|
||||
// lifecycle
|
||||
onBeforeMount,
|
||||
onMounted,
|
||||
onBeforeUpdate,
|
||||
onUpdated,
|
||||
onBeforeUnmount,
|
||||
onUnmounted,
|
||||
onErrorCaptured,
|
||||
onActivated,
|
||||
onDeactivated,
|
||||
onServerPrefetch,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
|
||||
// v2 only
|
||||
set,
|
||||
del,
|
||||
|
||||
// v3 compat
|
||||
h,
|
||||
getCurrentInstance,
|
||||
useSlots,
|
||||
useAttrs,
|
||||
mergeDefaults,
|
||||
nextTick,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
defineComponent,
|
||||
defineAsyncComponent
|
||||
} = Vue
|
|
@ -1,58 +1,60 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// APIController handles the main API routes for the 3x-ui panel, including inbounds and server management.
|
||||
type APIController struct {
|
||||
BaseController
|
||||
inboundController *InboundController
|
||||
serverController *ServerController
|
||||
Tgbot service.Tgbot
|
||||
}
|
||||
|
||||
// NewAPIController creates a new APIController instance and initializes its routes.
|
||||
func NewAPIController(g *gin.RouterGroup) *APIController {
|
||||
a := &APIController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Main API group
|
||||
api := g.Group("/panel/api")
|
||||
api.Use(a.checkAPIAuth)
|
||||
g = g.Group("/panel/api/inbounds")
|
||||
g.Use(a.checkLogin)
|
||||
|
||||
// Inbounds API
|
||||
inbounds := api.Group("/inbounds")
|
||||
a.inboundController = NewInboundController(inbounds)
|
||||
a.inboundController = NewInboundController(g)
|
||||
|
||||
// Server API
|
||||
server := api.Group("/server")
|
||||
a.serverController = NewServerController(server)
|
||||
inboundRoutes := []struct {
|
||||
Method string
|
||||
Path string
|
||||
Handler gin.HandlerFunc
|
||||
}{
|
||||
{"GET", "/createbackup", a.createBackup},
|
||||
{"GET", "/list", a.inboundController.getInbounds},
|
||||
{"GET", "/get/:id", a.inboundController.getInbound},
|
||||
{"GET", "/getClientTraffics/:email", a.inboundController.getClientTraffics},
|
||||
{"GET", "/getClientTrafficsById/:id", a.inboundController.getClientTrafficsById},
|
||||
{"POST", "/add", a.inboundController.addInbound},
|
||||
{"POST", "/del/:id", a.inboundController.delInbound},
|
||||
{"POST", "/update/:id", a.inboundController.updateInbound},
|
||||
{"POST", "/clientIps/:email", a.inboundController.getClientIps},
|
||||
{"POST", "/clearClientIps/:email", a.inboundController.clearClientIps},
|
||||
{"POST", "/addClient", a.inboundController.addInboundClient},
|
||||
{"POST", "/:id/delClient/:clientId", a.inboundController.delInboundClient},
|
||||
{"POST", "/updateClient/:clientId", a.inboundController.updateInboundClient},
|
||||
{"POST", "/:id/resetClientTraffic/:email", a.inboundController.resetClientTraffic},
|
||||
{"POST", "/resetAllTraffics", a.inboundController.resetAllTraffics},
|
||||
{"POST", "/resetAllClientTraffics/:id", a.inboundController.resetAllClientTraffics},
|
||||
{"POST", "/delDepletedClients/:id", a.inboundController.delDepletedClients},
|
||||
{"POST", "/onlines", a.inboundController.onlines},
|
||||
{"POST", "/updateClientTraffic/:email", a.inboundController.updateClientTraffic},
|
||||
}
|
||||
|
||||
// Extra routes
|
||||
api.GET("/backuptotgbot", a.BackuptoTgbot)
|
||||
for _, route := range inboundRoutes {
|
||||
g.Handle(route.Method, route.Path, route.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
// BackuptoTgbot sends a backup of the panel data to Telegram bot admins.
|
||||
func (a *APIController) BackuptoTgbot(c *gin.Context) {
|
||||
func (a *APIController) createBackup(c *gin.Context) {
|
||||
a.Tgbot.SendBackupToAdmins()
|
||||
}
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
// Package controller provides HTTP request handlers and controllers for the 3x-ui web management panel.
|
||||
// It handles routing, authentication, and API endpoints for managing Xray inbounds, settings, and more.
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/locale"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/locale"
|
||||
"x-ui/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// BaseController provides common functionality for all controllers, including authentication checks.
|
||||
type BaseController struct{}
|
||||
|
||||
// checkLogin is a middleware that verifies user authentication and handles unauthorized access.
|
||||
func (a *BaseController) checkLogin(c *gin.Context) {
|
||||
if !session.IsLogin(c) {
|
||||
if isAjax(c) {
|
||||
|
@ -29,7 +25,6 @@ func (a *BaseController) checkLogin(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// I18nWeb retrieves an internationalized message for the web interface based on the current locale.
|
||||
func I18nWeb(c *gin.Context, name string, params ...string) string {
|
||||
anyfunc, funcExists := c.Get("I18n")
|
||||
if !funcExists {
|
||||
|
|
|
@ -5,34 +5,28 @@ import (
|
|||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
"x-ui/database/model"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// InboundController handles HTTP requests related to Xray inbounds management.
|
||||
type InboundController struct {
|
||||
inboundService service.InboundService
|
||||
xrayService service.XrayService
|
||||
}
|
||||
|
||||
// NewInboundController creates a new InboundController and sets up its routes.
|
||||
func NewInboundController(g *gin.RouterGroup) *InboundController {
|
||||
a := &InboundController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
// initRouter initializes the routes for inbound-related operations.
|
||||
func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/inbound")
|
||||
|
||||
g.GET("/list", a.getInbounds)
|
||||
g.GET("/get/:id", a.getInbound)
|
||||
g.GET("/getClientTraffics/:email", a.getClientTraffics)
|
||||
g.GET("/getClientTrafficsById/:id", a.getClientTrafficsById)
|
||||
|
||||
g.POST("/list", a.getInbounds)
|
||||
g.POST("/add", a.addInbound)
|
||||
g.POST("/del/:id", a.delInbound)
|
||||
g.POST("/update/:id", a.updateInbound)
|
||||
|
@ -47,12 +41,8 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
|||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||
g.POST("/import", a.importInbound)
|
||||
g.POST("/onlines", a.onlines)
|
||||
g.POST("/lastOnline", a.lastOnline)
|
||||
g.POST("/updateClientTraffic/:email", a.updateClientTraffic)
|
||||
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
|
||||
}
|
||||
|
||||
// getInbounds retrieves the list of inbounds for the logged-in user.
|
||||
func (a *InboundController) getInbounds(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
inbounds, err := a.inboundService.GetInbounds(user.Id)
|
||||
|
@ -63,7 +53,6 @@ func (a *InboundController) getInbounds(c *gin.Context) {
|
|||
jsonObj(c, inbounds, nil)
|
||||
}
|
||||
|
||||
// getInbound retrieves a specific inbound by its ID.
|
||||
func (a *InboundController) getInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
|
@ -78,7 +67,6 @@ func (a *InboundController) getInbound(c *gin.Context) {
|
|||
jsonObj(c, inbound, nil)
|
||||
}
|
||||
|
||||
// getClientTraffics retrieves client traffic information by email.
|
||||
func (a *InboundController) getClientTraffics(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
|
||||
|
@ -89,7 +77,6 @@ func (a *InboundController) getClientTraffics(c *gin.Context) {
|
|||
jsonObj(c, clientTraffics, nil)
|
||||
}
|
||||
|
||||
// getClientTrafficsById retrieves client traffic information by inbound ID.
|
||||
func (a *InboundController) getClientTrafficsById(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
clientTraffics, err := a.inboundService.GetClientTrafficByID(id)
|
||||
|
@ -100,7 +87,6 @@ func (a *InboundController) getClientTrafficsById(c *gin.Context) {
|
|||
jsonObj(c, clientTraffics, nil)
|
||||
}
|
||||
|
||||
// addInbound creates a new inbound configuration.
|
||||
func (a *InboundController) addInbound(c *gin.Context) {
|
||||
inbound := &model.Inbound{}
|
||||
err := c.ShouldBind(inbound)
|
||||
|
@ -116,7 +102,8 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
|||
inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
|
||||
}
|
||||
|
||||
inbound, needRestart, err := a.inboundService.AddInbound(inbound)
|
||||
needRestart := false
|
||||
inbound, needRestart, err = a.inboundService.AddInbound(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
|
@ -127,14 +114,14 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// delInbound deletes an inbound configuration by its ID.
|
||||
func (a *InboundController) delInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), err)
|
||||
return
|
||||
}
|
||||
needRestart, err := a.inboundService.DelInbound(id)
|
||||
needRestart := true
|
||||
needRestart, err = a.inboundService.DelInbound(id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
|
@ -145,7 +132,6 @@ func (a *InboundController) delInbound(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// updateInbound updates an existing inbound configuration.
|
||||
func (a *InboundController) updateInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
|
@ -160,7 +146,8 @@ func (a *InboundController) updateInbound(c *gin.Context) {
|
|||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
||||
return
|
||||
}
|
||||
inbound, needRestart, err := a.inboundService.UpdateInbound(inbound)
|
||||
needRestart := true
|
||||
inbound, needRestart, err = a.inboundService.UpdateInbound(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
|
@ -171,7 +158,6 @@ func (a *InboundController) updateInbound(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// getClientIps retrieves the IP addresses associated with a client by email.
|
||||
func (a *InboundController) getClientIps(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
|
@ -184,7 +170,6 @@ func (a *InboundController) getClientIps(c *gin.Context) {
|
|||
jsonObj(c, ips, nil)
|
||||
}
|
||||
|
||||
// clearClientIps clears the IP addresses for a client by email.
|
||||
func (a *InboundController) clearClientIps(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
|
@ -196,7 +181,6 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
|
|||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.logCleanSuccess"), nil)
|
||||
}
|
||||
|
||||
// addInboundClient adds a new client to an existing inbound.
|
||||
func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||
data := &model.Inbound{}
|
||||
err := c.ShouldBind(data)
|
||||
|
@ -205,7 +189,9 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
needRestart, err := a.inboundService.AddInboundClient(data)
|
||||
needRestart := true
|
||||
|
||||
needRestart, err = a.inboundService.AddInboundClient(data)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
|
@ -216,7 +202,6 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// delInboundClient deletes a client from an inbound by inbound ID and client ID.
|
||||
func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
|
@ -225,7 +210,9 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
|
|||
}
|
||||
clientId := c.Param("clientId")
|
||||
|
||||
needRestart, err := a.inboundService.DelInboundClient(id, clientId)
|
||||
needRestart := true
|
||||
|
||||
needRestart, err = a.inboundService.DelInboundClient(id, clientId)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
|
@ -236,7 +223,6 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// updateInboundClient updates a client's configuration in an inbound.
|
||||
func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||
clientId := c.Param("clientId")
|
||||
|
||||
|
@ -247,7 +233,9 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
needRestart, err := a.inboundService.UpdateInboundClient(inbound, clientId)
|
||||
needRestart := true
|
||||
|
||||
needRestart, err = a.inboundService.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
|
@ -258,7 +246,6 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// resetClientTraffic resets the traffic counter for a specific client in an inbound.
|
||||
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
|
@ -278,7 +265,6 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// resetAllTraffics resets all traffic counters across all inbounds.
|
||||
func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
||||
err := a.inboundService.ResetAllTraffics()
|
||||
if err != nil {
|
||||
|
@ -290,7 +276,6 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
|||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllTrafficSuccess"), nil)
|
||||
}
|
||||
|
||||
// resetAllClientTraffics resets traffic counters for all clients in a specific inbound.
|
||||
func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
|
@ -308,7 +293,6 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
|||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil)
|
||||
}
|
||||
|
||||
// importInbound imports an inbound configuration from provided data.
|
||||
func (a *InboundController) importInbound(c *gin.Context) {
|
||||
inbound := &model.Inbound{}
|
||||
err := json.Unmarshal([]byte(c.PostForm("data")), inbound)
|
||||
|
@ -338,7 +322,6 @@ func (a *InboundController) importInbound(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// delDepletedClients deletes clients in an inbound who have exhausted their traffic limits.
|
||||
func (a *InboundController) delDepletedClients(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
|
@ -353,18 +336,10 @@ func (a *InboundController) delDepletedClients(c *gin.Context) {
|
|||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.delDepletedClientsSuccess"), nil)
|
||||
}
|
||||
|
||||
// onlines retrieves the list of currently online clients.
|
||||
func (a *InboundController) onlines(c *gin.Context) {
|
||||
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
||||
}
|
||||
|
||||
// lastOnline retrieves the last online timestamps for clients.
|
||||
func (a *InboundController) lastOnline(c *gin.Context) {
|
||||
data, err := a.inboundService.GetClientsLastOnline()
|
||||
jsonObj(c, data, err)
|
||||
}
|
||||
|
||||
// updateClientTraffic updates the traffic statistics for a client by email.
|
||||
func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
|
@ -389,24 +364,3 @@ func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
|||
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
|
||||
}
|
||||
|
||||
// delInboundClientByEmail deletes a client from an inbound by email address.
|
||||
func (a *InboundController) delInboundClientByEmail(c *gin.Context) {
|
||||
inboundId, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, "Invalid inbound ID", err)
|
||||
return
|
||||
}
|
||||
|
||||
email := c.Param("email")
|
||||
needRestart, err := a.inboundService.DelInboundClientByEmail(inboundId, email)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Failed to delete client by email", err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonMsg(c, "Client deleted successfully", nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,22 +5,20 @@ import (
|
|||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// LoginForm represents the login request structure.
|
||||
type LoginForm struct {
|
||||
Username string `json:"username" form:"username"`
|
||||
Password string `json:"password" form:"password"`
|
||||
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
|
||||
Username string `json:"username" form:"username"`
|
||||
Password string `json:"password" form:"password"`
|
||||
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
|
||||
}
|
||||
|
||||
// IndexController handles the main index and login-related routes.
|
||||
type IndexController struct {
|
||||
BaseController
|
||||
|
||||
|
@ -29,23 +27,19 @@ type IndexController struct {
|
|||
tgbot service.Tgbot
|
||||
}
|
||||
|
||||
// NewIndexController creates a new IndexController and initializes its routes.
|
||||
func NewIndexController(g *gin.RouterGroup) *IndexController {
|
||||
a := &IndexController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
// initRouter sets up the routes for index, login, logout, and two-factor authentication.
|
||||
func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/", a.index)
|
||||
g.GET("/logout", a.logout)
|
||||
|
||||
g.POST("/login", a.login)
|
||||
g.GET("/logout", a.logout)
|
||||
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
|
||||
}
|
||||
|
||||
// index handles the root route, redirecting logged-in users to the panel or showing the login page.
|
||||
func (a *IndexController) index(c *gin.Context) {
|
||||
if session.IsLogin(c) {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "panel/")
|
||||
|
@ -54,7 +48,6 @@ func (a *IndexController) index(c *gin.Context) {
|
|||
html(c, "login.html", "pages.login.title", nil)
|
||||
}
|
||||
|
||||
// login handles user authentication and session creation.
|
||||
func (a *IndexController) login(c *gin.Context) {
|
||||
var form LoginForm
|
||||
|
||||
|
@ -102,7 +95,6 @@ func (a *IndexController) login(c *gin.Context) {
|
|||
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil)
|
||||
}
|
||||
|
||||
// logout handles user logout by clearing the session and redirecting to the login page.
|
||||
func (a *IndexController) logout(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
if user != nil {
|
||||
|
@ -115,7 +107,6 @@ func (a *IndexController) logout(c *gin.Context) {
|
|||
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
||||
}
|
||||
|
||||
// getTwoFactorEnable retrieves the current status of two-factor authentication.
|
||||
func (a *IndexController) getTwoFactorEnable(c *gin.Context) {
|
||||
status, err := a.settingService.GetTwoFactorEnable()
|
||||
if err == nil {
|
||||
|
|
|
@ -4,52 +4,44 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/web/global"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
|
||||
|
||||
// ServerController handles server management and status-related operations.
|
||||
type ServerController struct {
|
||||
BaseController
|
||||
|
||||
serverService service.ServerService
|
||||
settingService service.SettingService
|
||||
|
||||
lastStatus *service.Status
|
||||
lastStatus *service.Status
|
||||
lastGetStatusTime time.Time
|
||||
|
||||
lastVersions []string
|
||||
lastGetVersionsTime int64 // unix seconds
|
||||
lastGetVersionsTime time.Time
|
||||
}
|
||||
|
||||
// NewServerController creates a new ServerController, initializes routes, and starts background tasks.
|
||||
func NewServerController(g *gin.RouterGroup) *ServerController {
|
||||
a := &ServerController{}
|
||||
a := &ServerController{
|
||||
lastGetStatusTime: time.Now(),
|
||||
}
|
||||
a.initRouter(g)
|
||||
a.startTask()
|
||||
return a
|
||||
}
|
||||
|
||||
// initRouter sets up the routes for server status, Xray management, and utility endpoints.
|
||||
func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/server")
|
||||
|
||||
g.GET("/status", a.status)
|
||||
g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket)
|
||||
g.GET("/getXrayVersion", a.getXrayVersion)
|
||||
g.GET("/getConfigJson", a.getConfigJson)
|
||||
g.GET("/getDb", a.getDb)
|
||||
g.GET("/getNewUUID", a.getNewUUID)
|
||||
g.GET("/getNewX25519Cert", a.getNewX25519Cert)
|
||||
g.GET("/getNewmldsa65", a.getNewmldsa65)
|
||||
g.GET("/getNewmlkem768", a.getNewmlkem768)
|
||||
g.GET("/getNewVlessEnc", a.getNewVlessEnc)
|
||||
|
||||
g.Use(a.checkLogin)
|
||||
g.POST("/status", a.status)
|
||||
g.POST("/getXrayVersion", a.getXrayVersion)
|
||||
g.POST("/stopXrayService", a.stopXrayService)
|
||||
g.POST("/restartXrayService", a.restartXrayService)
|
||||
g.POST("/installXray/:version", a.installXray)
|
||||
|
@ -57,61 +49,39 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
|||
g.POST("/updateGeofile/:fileName", a.updateGeofile)
|
||||
g.POST("/logs/:count", a.getLogs)
|
||||
g.POST("/xraylogs/:count", a.getXrayLogs)
|
||||
g.POST("/getConfigJson", a.getConfigJson)
|
||||
g.GET("/getDb", a.getDb)
|
||||
g.POST("/importDB", a.importDB)
|
||||
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
|
||||
g.POST("/getNewmldsa65", a.getNewmldsa65)
|
||||
g.POST("/getNewEchCert", a.getNewEchCert)
|
||||
}
|
||||
|
||||
// refreshStatus updates the cached server status and collects CPU history.
|
||||
func (a *ServerController) refreshStatus() {
|
||||
a.lastStatus = a.serverService.GetStatus(a.lastStatus)
|
||||
// collect cpu history when status is fresh
|
||||
if a.lastStatus != nil {
|
||||
a.serverService.AppendCpuSample(time.Now(), a.lastStatus.Cpu)
|
||||
}
|
||||
}
|
||||
|
||||
// startTask initiates background tasks for continuous status monitoring.
|
||||
func (a *ServerController) startTask() {
|
||||
webServer := global.GetWebServer()
|
||||
c := webServer.GetCron()
|
||||
c.AddFunc("@every 2s", func() {
|
||||
// Always refresh to keep CPU history collected continuously.
|
||||
// Sampling is lightweight and capped to ~6 hours in memory.
|
||||
now := time.Now()
|
||||
if now.Sub(a.lastGetStatusTime) > time.Minute*3 {
|
||||
return
|
||||
}
|
||||
a.refreshStatus()
|
||||
})
|
||||
}
|
||||
|
||||
// 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) {
|
||||
a.lastGetStatusTime = time.Now()
|
||||
|
||||
// getCpuHistoryBucket retrieves aggregated CPU usage history based on the specified time bucket.
|
||||
func (a *ServerController) getCpuHistoryBucket(c *gin.Context) {
|
||||
bucketStr := c.Param("bucket")
|
||||
bucket, err := strconv.Atoi(bucketStr)
|
||||
if err != nil || bucket <= 0 {
|
||||
jsonMsg(c, "invalid bucket", fmt.Errorf("bad bucket"))
|
||||
return
|
||||
}
|
||||
allowed := map[int]bool{
|
||||
2: true, // Real-time view
|
||||
30: true, // 30s intervals
|
||||
60: true, // 1m intervals
|
||||
120: true, // 2m intervals
|
||||
180: true, // 3m intervals
|
||||
300: true, // 5m intervals
|
||||
}
|
||||
if !allowed[bucket] {
|
||||
jsonMsg(c, "invalid bucket", fmt.Errorf("unsupported bucket"))
|
||||
return
|
||||
}
|
||||
points := a.serverService.AggregateCpuHistory(bucket, 60)
|
||||
jsonObj(c, points, nil)
|
||||
jsonObj(c, a.lastStatus, nil)
|
||||
}
|
||||
|
||||
// getXrayVersion retrieves available Xray versions, with caching for 1 minute.
|
||||
func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||
now := time.Now().Unix()
|
||||
if now-a.lastGetVersionsTime <= 60 { // 1 minute cache
|
||||
now := time.Now()
|
||||
if now.Sub(a.lastGetVersionsTime) <= time.Minute {
|
||||
jsonObj(c, a.lastVersions, nil)
|
||||
return
|
||||
}
|
||||
|
@ -123,35 +93,25 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
|
|||
}
|
||||
|
||||
a.lastVersions = versions
|
||||
a.lastGetVersionsTime = now
|
||||
a.lastGetVersionsTime = time.Now()
|
||||
|
||||
jsonObj(c, versions, nil)
|
||||
}
|
||||
|
||||
// installXray installs or updates Xray to the specified version.
|
||||
func (a *ServerController) installXray(c *gin.Context) {
|
||||
version := c.Param("version")
|
||||
err := a.serverService.UpdateXray(version)
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err)
|
||||
}
|
||||
|
||||
// updateGeofile updates the specified geo file for Xray.
|
||||
func (a *ServerController) updateGeofile(c *gin.Context) {
|
||||
fileName := c.Param("fileName")
|
||||
|
||||
// Validate the filename for security (prevent path traversal attacks)
|
||||
if fileName != "" && !a.serverService.IsValidGeofileName(fileName) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"),
|
||||
fmt.Errorf("invalid filename: contains unsafe characters or path traversal patterns"))
|
||||
return
|
||||
}
|
||||
|
||||
err := a.serverService.UpdateGeofile(fileName)
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err)
|
||||
}
|
||||
|
||||
// stopXrayService stops the Xray service.
|
||||
func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||
a.lastGetStatusTime = time.Now()
|
||||
err := a.serverService.StopXrayService()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err)
|
||||
|
@ -160,7 +120,6 @@ func (a *ServerController) stopXrayService(c *gin.Context) {
|
|||
jsonMsg(c, I18nWeb(c, "pages.xray.stopSuccess"), err)
|
||||
}
|
||||
|
||||
// restartXrayService restarts the Xray service.
|
||||
func (a *ServerController) restartXrayService(c *gin.Context) {
|
||||
err := a.serverService.RestartXrayService()
|
||||
if err != nil {
|
||||
|
@ -170,7 +129,6 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
|
|||
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) {
|
||||
count := c.Param("count")
|
||||
level := c.PostForm("level")
|
||||
|
@ -179,7 +137,6 @@ func (a *ServerController) getLogs(c *gin.Context) {
|
|||
jsonObj(c, logs, nil)
|
||||
}
|
||||
|
||||
// getXrayLogs retrieves Xray logs with filtering options for direct, blocked, and proxy traffic.
|
||||
func (a *ServerController) getXrayLogs(c *gin.Context) {
|
||||
count := c.Param("count")
|
||||
filter := c.PostForm("filter")
|
||||
|
@ -224,7 +181,6 @@ func (a *ServerController) getXrayLogs(c *gin.Context) {
|
|||
jsonObj(c, logs, nil)
|
||||
}
|
||||
|
||||
// getConfigJson retrieves the Xray configuration as JSON.
|
||||
func (a *ServerController) getConfigJson(c *gin.Context) {
|
||||
configJson, err := a.serverService.GetConfigJson()
|
||||
if err != nil {
|
||||
|
@ -234,7 +190,6 @@ func (a *ServerController) getConfigJson(c *gin.Context) {
|
|||
jsonObj(c, configJson, nil)
|
||||
}
|
||||
|
||||
// getDb downloads the database file.
|
||||
func (a *ServerController) getDb(c *gin.Context) {
|
||||
db, err := a.serverService.GetDb()
|
||||
if err != nil {
|
||||
|
@ -262,7 +217,6 @@ func isValidFilename(filename string) bool {
|
|||
return filenameRegex.MatchString(filename)
|
||||
}
|
||||
|
||||
// importDB imports a database file and restarts the Xray service.
|
||||
func (a *ServerController) importDB(c *gin.Context) {
|
||||
// Get the file from the request body
|
||||
file, _, err := c.Request.FormFile("db")
|
||||
|
@ -273,7 +227,9 @@ func (a *ServerController) importDB(c *gin.Context) {
|
|||
defer file.Close()
|
||||
// Always restart Xray before return
|
||||
defer a.serverService.RestartXrayService()
|
||||
// lastGetStatusTime removed; no longer needed
|
||||
defer func() {
|
||||
a.lastGetStatusTime = time.Now()
|
||||
}()
|
||||
// Import it
|
||||
err = a.serverService.ImportDB(file)
|
||||
if err != nil {
|
||||
|
@ -283,7 +239,6 @@ func (a *ServerController) importDB(c *gin.Context) {
|
|||
jsonObj(c, I18nWeb(c, "pages.index.importDatabaseSuccess"), nil)
|
||||
}
|
||||
|
||||
// getNewX25519Cert generates a new X25519 certificate.
|
||||
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
|
||||
cert, err := a.serverService.GetNewX25519Cert()
|
||||
if err != nil {
|
||||
|
@ -293,7 +248,6 @@ func (a *ServerController) getNewX25519Cert(c *gin.Context) {
|
|||
jsonObj(c, cert, nil)
|
||||
}
|
||||
|
||||
// getNewmldsa65 generates a new ML-DSA-65 key.
|
||||
func (a *ServerController) getNewmldsa65(c *gin.Context) {
|
||||
cert, err := a.serverService.GetNewmldsa65()
|
||||
if err != nil {
|
||||
|
@ -303,7 +257,6 @@ func (a *ServerController) getNewmldsa65(c *gin.Context) {
|
|||
jsonObj(c, cert, nil)
|
||||
}
|
||||
|
||||
// getNewEchCert generates a new ECH certificate for the given SNI.
|
||||
func (a *ServerController) getNewEchCert(c *gin.Context) {
|
||||
sni := c.PostForm("sni")
|
||||
cert, err := a.serverService.GetNewEchCert(sni)
|
||||
|
@ -313,34 +266,3 @@ func (a *ServerController) getNewEchCert(c *gin.Context) {
|
|||
}
|
||||
jsonObj(c, cert, nil)
|
||||
}
|
||||
|
||||
// getNewVlessEnc generates a new VLESS encryption key.
|
||||
func (a *ServerController) getNewVlessEnc(c *gin.Context) {
|
||||
out, err := a.serverService.GetNewVlessEnc()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.getNewVlessEncError"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, out, nil)
|
||||
}
|
||||
|
||||
// getNewUUID generates a new UUID.
|
||||
func (a *ServerController) getNewUUID(c *gin.Context) {
|
||||
uuidResp, err := a.serverService.GetNewUUID()
|
||||
if err != nil {
|
||||
jsonMsg(c, "Failed to generate UUID", err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonObj(c, uuidResp, nil)
|
||||
}
|
||||
|
||||
// getNewmlkem768 generates a new ML-KEM-768 key.
|
||||
func (a *ServerController) getNewmlkem768(c *gin.Context) {
|
||||
out, err := a.serverService.GetNewmlkem768()
|
||||
if err != nil {
|
||||
jsonMsg(c, "Failed to generate mlkem768 keys", err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, out, nil)
|
||||
}
|
||||
|
|
|
@ -4,15 +4,14 @@ import (
|
|||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/entity"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
"x-ui/util/crypto"
|
||||
"x-ui/web/entity"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// updateUserForm represents the form for updating user credentials.
|
||||
type updateUserForm struct {
|
||||
OldUsername string `json:"oldUsername" form:"oldUsername"`
|
||||
OldPassword string `json:"oldPassword" form:"oldPassword"`
|
||||
|
@ -20,21 +19,18 @@ type updateUserForm struct {
|
|||
NewPassword string `json:"newPassword" form:"newPassword"`
|
||||
}
|
||||
|
||||
// SettingController handles settings and user management operations.
|
||||
type SettingController struct {
|
||||
settingService service.SettingService
|
||||
userService service.UserService
|
||||
panelService service.PanelService
|
||||
}
|
||||
|
||||
// NewSettingController creates a new SettingController and initializes its routes.
|
||||
func NewSettingController(g *gin.RouterGroup) *SettingController {
|
||||
a := &SettingController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
// initRouter sets up the routes for settings management.
|
||||
func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/setting")
|
||||
|
||||
|
@ -46,7 +42,6 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
|||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||
}
|
||||
|
||||
// getAllSetting retrieves all current settings.
|
||||
func (a *SettingController) getAllSetting(c *gin.Context) {
|
||||
allSetting, err := a.settingService.GetAllSetting()
|
||||
if err != nil {
|
||||
|
@ -56,7 +51,6 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
|
|||
jsonObj(c, allSetting, nil)
|
||||
}
|
||||
|
||||
// getDefaultSettings retrieves the default settings based on the host.
|
||||
func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
||||
result, err := a.settingService.GetDefaultSettings(c.Request.Host)
|
||||
if err != nil {
|
||||
|
@ -66,7 +60,6 @@ func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
|||
jsonObj(c, result, nil)
|
||||
}
|
||||
|
||||
// updateSetting updates all settings with the provided data.
|
||||
func (a *SettingController) updateSetting(c *gin.Context) {
|
||||
allSetting := &entity.AllSetting{}
|
||||
err := c.ShouldBind(allSetting)
|
||||
|
@ -78,7 +71,6 @@ func (a *SettingController) updateSetting(c *gin.Context) {
|
|||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
}
|
||||
|
||||
// updateUser updates the current user's username and password.
|
||||
func (a *SettingController) updateUser(c *gin.Context) {
|
||||
form := &updateUserForm{}
|
||||
err := c.ShouldBind(form)
|
||||
|
@ -104,13 +96,11 @@ func (a *SettingController) updateUser(c *gin.Context) {
|
|||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
|
||||
}
|
||||
|
||||
// restartPanel restarts the panel service after a delay.
|
||||
func (a *SettingController) restartPanel(c *gin.Context) {
|
||||
err := a.panelService.RestartPanel(time.Second * 3)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.restartPanelSuccess"), err)
|
||||
}
|
||||
|
||||
// getDefaultXrayConfig retrieves the default Xray configuration.
|
||||
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
|
||||
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
|
||||
if err != nil {
|
||||
|
|
|
@ -5,14 +5,13 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/entity"
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/entity"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// getRemoteIp extracts the real IP address from the request headers or remote address.
|
||||
func getRemoteIp(c *gin.Context) string {
|
||||
value := c.GetHeader("X-Real-IP")
|
||||
if value != "" {
|
||||
|
@ -28,17 +27,14 @@ func getRemoteIp(c *gin.Context) string {
|
|||
return ip
|
||||
}
|
||||
|
||||
// jsonMsg sends a JSON response with a message and error status.
|
||||
func jsonMsg(c *gin.Context, msg string, err error) {
|
||||
jsonMsgObj(c, msg, nil, err)
|
||||
}
|
||||
|
||||
// jsonObj sends a JSON response with an object and error status.
|
||||
func jsonObj(c *gin.Context, obj any, err error) {
|
||||
jsonMsgObj(c, "", obj, err)
|
||||
}
|
||||
|
||||
// jsonMsgObj sends a JSON response with a message, object, and error status.
|
||||
func jsonMsgObj(c *gin.Context, msg string, obj any, err error) {
|
||||
m := entity.Msg{
|
||||
Obj: obj,
|
||||
|
@ -56,7 +52,6 @@ func jsonMsgObj(c *gin.Context, msg string, obj any, err error) {
|
|||
c.JSON(http.StatusOK, m)
|
||||
}
|
||||
|
||||
// pureJsonMsg sends a pure JSON message response with custom status code.
|
||||
func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) {
|
||||
c.JSON(statusCode, entity.Msg{
|
||||
Success: success,
|
||||
|
@ -64,7 +59,6 @@ func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) {
|
|||
})
|
||||
}
|
||||
|
||||
// html renders an HTML template with the provided data and title.
|
||||
func html(c *gin.Context, name string, title string, data gin.H) {
|
||||
if data == nil {
|
||||
data = gin.H{}
|
||||
|
@ -87,7 +81,6 @@ func html(c *gin.Context, name string, title string, data gin.H) {
|
|||
c.HTML(http.StatusOK, name, getContext(data))
|
||||
}
|
||||
|
||||
// getContext adds version and other context data to the provided gin.H.
|
||||
func getContext(h gin.H) gin.H {
|
||||
a := gin.H{
|
||||
"cur_ver": config.GetVersion(),
|
||||
|
@ -98,7 +91,6 @@ func getContext(h gin.H) gin.H {
|
|||
return a
|
||||
}
|
||||
|
||||
// isAjax checks if the request is an AJAX request.
|
||||
func isAjax(c *gin.Context) bool {
|
||||
return c.GetHeader("X-Requested-With") == "XMLHttpRequest"
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// XraySettingController handles Xray configuration and settings operations.
|
||||
type XraySettingController struct {
|
||||
XraySettingService service.XraySettingService
|
||||
SettingService service.SettingService
|
||||
|
@ -16,27 +15,24 @@ type XraySettingController struct {
|
|||
WarpService service.WarpService
|
||||
}
|
||||
|
||||
// NewXraySettingController creates a new XraySettingController and initializes its routes.
|
||||
func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
|
||||
a := &XraySettingController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
// initRouter sets up the routes for Xray settings management.
|
||||
func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/xray")
|
||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||
g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
|
||||
g.GET("/getXrayResult", a.getXrayResult)
|
||||
|
||||
g.POST("/", a.getXraySetting)
|
||||
g.POST("/warp/:action", a.warp)
|
||||
g.POST("/update", a.updateSetting)
|
||||
g.GET("/getXrayResult", a.getXrayResult)
|
||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||
g.POST("/warp/:action", a.warp)
|
||||
g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
|
||||
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
||||
}
|
||||
|
||||
// getXraySetting retrieves the Xray configuration template and inbound tags.
|
||||
func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
||||
xraySetting, err := a.SettingService.GetXrayConfigTemplate()
|
||||
if err != nil {
|
||||
|
@ -52,14 +48,12 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
|||
jsonObj(c, xrayResponse, nil)
|
||||
}
|
||||
|
||||
// updateSetting updates the Xray configuration settings.
|
||||
func (a *XraySettingController) updateSetting(c *gin.Context) {
|
||||
xraySetting := c.PostForm("xraySetting")
|
||||
err := a.XraySettingService.SaveXraySetting(xraySetting)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
}
|
||||
|
||||
// getDefaultXrayConfig retrieves the default Xray configuration.
|
||||
func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
|
||||
defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig()
|
||||
if err != nil {
|
||||
|
@ -69,12 +63,10 @@ func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
|
|||
jsonObj(c, defaultJsonConfig, nil)
|
||||
}
|
||||
|
||||
// getXrayResult retrieves the current Xray service result.
|
||||
func (a *XraySettingController) getXrayResult(c *gin.Context) {
|
||||
jsonObj(c, a.XrayService.GetXrayResult(), nil)
|
||||
}
|
||||
|
||||
// warp handles Warp-related operations based on the action parameter.
|
||||
func (a *XraySettingController) warp(c *gin.Context) {
|
||||
action := c.Param("action")
|
||||
var resp string
|
||||
|
@ -98,7 +90,6 @@ func (a *XraySettingController) warp(c *gin.Context) {
|
|||
jsonObj(c, resp, err)
|
||||
}
|
||||
|
||||
// getOutboundsTraffic retrieves the traffic statistics for outbounds.
|
||||
func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
|
||||
outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
|
||||
if err != nil {
|
||||
|
@ -108,7 +99,6 @@ func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
|
|||
jsonObj(c, outboundsTraffic, nil)
|
||||
}
|
||||
|
||||
// resetOutboundsTraffic resets the traffic statistics for the specified outbound tag.
|
||||
func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
|
||||
tag := c.PostForm("tag")
|
||||
err := a.OutboundService.ResetOutboundTraffic(tag)
|
||||
|
|
|
@ -4,22 +4,20 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// XUIController is the main controller for the X-UI panel, managing sub-controllers.
|
||||
type XUIController struct {
|
||||
BaseController
|
||||
|
||||
inboundController *InboundController
|
||||
settingController *SettingController
|
||||
xraySettingController *XraySettingController
|
||||
}
|
||||
|
||||
// NewXUIController creates a new XUIController and initializes its routes.
|
||||
func NewXUIController(g *gin.RouterGroup) *XUIController {
|
||||
a := &XUIController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
// initRouter sets up the main panel routes and initializes sub-controllers.
|
||||
func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/panel")
|
||||
g.Use(a.checkLogin)
|
||||
|
@ -29,26 +27,23 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
|||
g.GET("/settings", a.settings)
|
||||
g.GET("/xray", a.xraySettings)
|
||||
|
||||
a.inboundController = NewInboundController(g)
|
||||
a.settingController = NewSettingController(g)
|
||||
a.xraySettingController = NewXraySettingController(g)
|
||||
}
|
||||
|
||||
// index renders the main panel index page.
|
||||
func (a *XUIController) index(c *gin.Context) {
|
||||
html(c, "index.html", "pages.index.title", nil)
|
||||
}
|
||||
|
||||
// inbounds renders the inbounds management page.
|
||||
func (a *XUIController) inbounds(c *gin.Context) {
|
||||
html(c, "inbounds.html", "pages.inbounds.title", nil)
|
||||
}
|
||||
|
||||
// settings renders the settings management page.
|
||||
func (a *XUIController) settings(c *gin.Context) {
|
||||
html(c, "settings.html", "pages.settings.title", nil)
|
||||
}
|
||||
|
||||
// xraySettings renders the Xray settings page.
|
||||
func (a *XUIController) xraySettings(c *gin.Context) {
|
||||
html(c, "xray.html", "pages.xray.title", nil)
|
||||
}
|
||||
|
|
|
@ -1,83 +1,91 @@
|
|||
// Package entity defines data structures and entities used by the web layer of the 3x-ui panel.
|
||||
package entity
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"math"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
"math"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||
"x-ui/util/common"
|
||||
)
|
||||
|
||||
// Msg represents a standard API response message with success status, message text, and optional data object.
|
||||
type Msg struct {
|
||||
Success bool `json:"success"` // Indicates if the operation was successful
|
||||
Msg string `json:"msg"` // Response message text
|
||||
Obj any `json:"obj"` // Optional data object
|
||||
Success bool `json:"success"`
|
||||
Msg string `json:"msg"`
|
||||
Obj any `json:"obj"`
|
||||
}
|
||||
|
||||
// AllSetting contains all configuration settings for the 3x-ui panel including web server, Telegram bot, and subscription settings.
|
||||
type AllSetting struct {
|
||||
// Web server settings
|
||||
WebListen string `json:"webListen" form:"webListen"` // Web server listen IP address
|
||||
WebDomain string `json:"webDomain" form:"webDomain"` // Web server domain for domain validation
|
||||
WebPort int `json:"webPort" form:"webPort"` // Web server port number
|
||||
WebCertFile string `json:"webCertFile" form:"webCertFile"` // Path to SSL certificate file for web server
|
||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` // Path to SSL private key file for web server
|
||||
WebBasePath string `json:"webBasePath" form:"webBasePath"` // Base path for web panel URLs
|
||||
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"` // Session maximum age in minutes
|
||||
|
||||
// UI settings
|
||||
PageSize int `json:"pageSize" form:"pageSize"` // Number of items per page in lists
|
||||
ExpireDiff int `json:"expireDiff" form:"expireDiff"` // Expiration warning threshold in days
|
||||
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"` // Traffic warning threshold percentage
|
||||
RemarkModel string `json:"remarkModel" form:"remarkModel"` // Remark model pattern for inbounds
|
||||
Datepicker string `json:"datepicker" form:"datepicker"` // Date picker format
|
||||
|
||||
// Telegram bot settings
|
||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` // Enable Telegram bot notifications
|
||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"` // Telegram bot token
|
||||
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"` // Proxy URL for Telegram bot
|
||||
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"` // Custom API server for Telegram bot
|
||||
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"` // Telegram chat ID for notifications
|
||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"` // Cron schedule for Telegram notifications
|
||||
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` // Enable database backup via Telegram
|
||||
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"` // Send login notifications
|
||||
TgCpu int `json:"tgCpu" form:"tgCpu"` // CPU usage threshold for alerts
|
||||
TgLang string `json:"tgLang" form:"tgLang"` // Telegram bot language
|
||||
|
||||
// Security settings
|
||||
TimeLocation string `json:"timeLocation" form:"timeLocation"` // Time zone location
|
||||
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"` // Enable two-factor authentication
|
||||
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"` // Two-factor authentication token
|
||||
|
||||
// Subscription server settings
|
||||
SubEnable bool `json:"subEnable" form:"subEnable"` // Enable subscription server
|
||||
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"` // Enable JSON subscription endpoint
|
||||
SubTitle string `json:"subTitle" form:"subTitle"` // Subscription title
|
||||
SubListen string `json:"subListen" form:"subListen"` // Subscription server listen IP
|
||||
SubPort int `json:"subPort" form:"subPort"` // Subscription server port
|
||||
SubPath string `json:"subPath" form:"subPath"` // Base path for subscription URLs
|
||||
SubDomain string `json:"subDomain" form:"subDomain"` // Domain for subscription server validation
|
||||
SubCertFile string `json:"subCertFile" form:"subCertFile"` // SSL certificate file for subscription server
|
||||
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"` // SSL private key file for subscription server
|
||||
SubUpdates int `json:"subUpdates" form:"subUpdates"` // Subscription update interval in minutes
|
||||
ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"` // Enable external traffic reporting
|
||||
ExternalTrafficInformURI string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"` // URI for external traffic reporting
|
||||
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"` // Encrypt subscription responses
|
||||
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"` // Show client information in subscriptions
|
||||
SubURI string `json:"subURI" form:"subURI"` // Subscription server URI
|
||||
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"` // Path for JSON subscription endpoint
|
||||
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"` // JSON subscription server URI
|
||||
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"` // JSON subscription fragment configuration
|
||||
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"` // JSON subscription noise configuration
|
||||
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` // JSON subscription mux configuration
|
||||
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"` // JSON subscription routing rules
|
||||
WebListen string `json:"webListen" form:"webListen"`
|
||||
WebDomain string `json:"webDomain" form:"webDomain"`
|
||||
WebPort int `json:"webPort" form:"webPort"`
|
||||
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
||||
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
||||
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
|
||||
PageSize int `json:"pageSize" form:"pageSize"`
|
||||
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
|
||||
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
|
||||
RemarkModel string `json:"remarkModel" form:"remarkModel"`
|
||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
||||
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"`
|
||||
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"`
|
||||
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
|
||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
||||
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
|
||||
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
|
||||
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
||||
TgLang string `json:"tgLang" form:"tgLang"`
|
||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
||||
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
||||
SubTitle string `json:"subTitle" form:"subTitle"`
|
||||
SubListen string `json:"subListen" form:"subListen"`
|
||||
SubPort int `json:"subPort" form:"subPort"`
|
||||
SubPath string `json:"subPath" form:"subPath"`
|
||||
SubDomain string `json:"subDomain" form:"subDomain"`
|
||||
SubCertFile string `json:"subCertFile" form:"subCertFile"`
|
||||
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
|
||||
SubUpdates int `json:"subUpdates" form:"subUpdates"`
|
||||
ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"`
|
||||
ExternalTrafficInformURI string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"`
|
||||
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
|
||||
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
|
||||
SubURI string `json:"subURI" form:"subURI"`
|
||||
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
|
||||
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
|
||||
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
|
||||
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"`
|
||||
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
|
||||
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
|
||||
Datepicker string `json:"datepicker" form:"datepicker"`
|
||||
// LDAP settings
|
||||
LdapEnable bool `json:"ldapEnable" form:"ldapEnable"`
|
||||
LdapHost string `json:"ldapHost" form:"ldapHost"`
|
||||
LdapPort int `json:"ldapPort" form:"ldapPort"`
|
||||
LdapUseTLS bool `json:"ldapUseTLS" form:"ldapUseTLS"`
|
||||
LdapBindDN string `json:"ldapBindDN" form:"ldapBindDN"`
|
||||
LdapPassword string `json:"ldapPassword" form:"ldapPassword"`
|
||||
LdapBaseDN string `json:"ldapBaseDN" form:"ldapBaseDN"`
|
||||
LdapUserFilter string `json:"ldapUserFilter" form:"ldapUserFilter"`
|
||||
LdapUserAttr string `json:"ldapUserAttr" form:"ldapUserAttr"` // e.g., mail or uid
|
||||
LdapVlessField string `json:"ldapVlessField" form:"ldapVlessField"`
|
||||
LdapSyncCron string `json:"ldapSyncCron" form:"ldapSyncCron"`
|
||||
// Generic flag configuration
|
||||
LdapFlagField string `json:"ldapFlagField" form:"ldapFlagField"`
|
||||
LdapTruthyValues string `json:"ldapTruthyValues" form:"ldapTruthyValues"`
|
||||
LdapInvertFlag bool `json:"ldapInvertFlag" form:"ldapInvertFlag"`
|
||||
LdapInboundTags string `json:"ldapInboundTags" form:"ldapInboundTags"`
|
||||
LdapAutoCreate bool `json:"ldapAutoCreate" form:"ldapAutoCreate"`
|
||||
LdapAutoDelete bool `json:"ldapAutoDelete" form:"ldapAutoDelete"`
|
||||
LdapDefaultTotalGB int `json:"ldapDefaultTotalGB" form:"ldapDefaultTotalGB"`
|
||||
LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays"`
|
||||
LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP"`
|
||||
}
|
||||
|
||||
// CheckValid validates all settings in the AllSetting struct, checking IP addresses, ports, SSL certificates, and other configuration values.
|
||||
func (s *AllSetting) CheckValid() error {
|
||||
if s.WebListen != "" {
|
||||
ip := net.ParseIP(s.WebListen)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// Package global provides global variables and interfaces for accessing web and subscription servers.
|
||||
package global
|
||||
|
||||
import (
|
||||
|
@ -13,33 +12,27 @@ var (
|
|||
subServer SubServer
|
||||
)
|
||||
|
||||
// WebServer interface defines methods for accessing the web server instance.
|
||||
type WebServer interface {
|
||||
GetCron() *cron.Cron // Get the cron scheduler
|
||||
GetCtx() context.Context // Get the server context
|
||||
GetCron() *cron.Cron
|
||||
GetCtx() context.Context
|
||||
}
|
||||
|
||||
// SubServer interface defines methods for accessing the subscription server instance.
|
||||
type SubServer interface {
|
||||
GetCtx() context.Context // Get the server context
|
||||
GetCtx() context.Context
|
||||
}
|
||||
|
||||
// SetWebServer sets the global web server instance.
|
||||
func SetWebServer(s WebServer) {
|
||||
webServer = s
|
||||
}
|
||||
|
||||
// GetWebServer returns the global web server instance.
|
||||
func GetWebServer() WebServer {
|
||||
return webServer
|
||||
}
|
||||
|
||||
// SetSubServer sets the global subscription server instance.
|
||||
func SetSubServer(s SubServer) {
|
||||
subServer = s
|
||||
}
|
||||
|
||||
// GetSubServer returns the global subscription server instance.
|
||||
func GetSubServer() SubServer {
|
||||
return subServer
|
||||
}
|
||||
|
|
|
@ -8,21 +8,18 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// HashEntry represents a stored hash entry with its value and timestamp.
|
||||
type HashEntry struct {
|
||||
Hash string // MD5 hash string
|
||||
Value string // Original value
|
||||
Timestamp time.Time // Time when the hash was created
|
||||
Hash string
|
||||
Value string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// HashStorage provides thread-safe storage for hash-value pairs with expiration.
|
||||
type HashStorage struct {
|
||||
sync.RWMutex
|
||||
Data map[string]HashEntry // Map of hash to entry
|
||||
Expiration time.Duration // Expiration duration for entries
|
||||
Data map[string]HashEntry
|
||||
Expiration time.Duration
|
||||
}
|
||||
|
||||
// NewHashStorage creates a new HashStorage instance with the specified expiration duration.
|
||||
func NewHashStorage(expiration time.Duration) *HashStorage {
|
||||
return &HashStorage{
|
||||
Data: make(map[string]HashEntry),
|
||||
|
@ -30,7 +27,6 @@ func NewHashStorage(expiration time.Duration) *HashStorage {
|
|||
}
|
||||
}
|
||||
|
||||
// SaveHash generates an MD5 hash for the given query string and stores it with a timestamp.
|
||||
func (h *HashStorage) SaveHash(query string) string {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
@ -49,7 +45,6 @@ func (h *HashStorage) SaveHash(query string) string {
|
|||
return md5HashString
|
||||
}
|
||||
|
||||
// GetValue retrieves the original value for the given hash, returning true if found.
|
||||
func (h *HashStorage) GetValue(hash string) (string, bool) {
|
||||
h.RLock()
|
||||
defer h.RUnlock()
|
||||
|
@ -59,13 +54,11 @@ func (h *HashStorage) GetValue(hash string) (string, bool) {
|
|||
return entry.Value, exists
|
||||
}
|
||||
|
||||
// IsMD5 checks if the given string is a valid 32-character MD5 hash.
|
||||
func (h *HashStorage) IsMD5(hash string) bool {
|
||||
match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash)
|
||||
return match
|
||||
}
|
||||
|
||||
// RemoveExpiredHashes removes all hash entries that have exceeded the expiration duration.
|
||||
func (h *HashStorage) RemoveExpiredHashes() {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
@ -79,7 +72,6 @@ func (h *HashStorage) RemoveExpiredHashes() {
|
|||
}
|
||||
}
|
||||
|
||||
// Reset clears all stored hash entries.
|
||||
func (h *HashStorage) Reset() {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
|
|
@ -2,21 +2,21 @@
|
|||
<template slot="actions" slot-scope="text, client, index">
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "qrCode" }}</template>
|
||||
<a-icon :style="{ fontSize: '22px', marginInlineStart: '14px' }" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
|
||||
<a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
||||
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
|
||||
<a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "info" }}</template>
|
||||
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
|
||||
<a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" :style="{ color: 'var(--color-primary-100)'}"></a-icon>
|
||||
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
|
||||
<a-icon :style="{ fontSize: '24px', cursor: 'pointer' }" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
|
@ -25,7 +25,7 @@
|
|||
</template>
|
||||
<a-popconfirm @confirm="delClient(record.id,client,false)" title='{{ i18n "pages.inbounds.deleteClientContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "delete"}}' ok-type="danger" cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" :style="{ color: '#e04141' }"></a-icon>
|
||||
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
|
||||
<a-icon :style="{ fontSize: '24px', cursor: 'pointer' }" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
|
@ -33,23 +33,18 @@
|
|||
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
|
||||
</template>
|
||||
<template slot="online" slot-scope="text, client, index">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content" >
|
||||
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
|
||||
</template>
|
||||
<template v-if="client.enable && isClientOnline(client.email)">
|
||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>{{ i18n "offline" }}</a-tag>
|
||||
</template>
|
||||
</a-popover>
|
||||
<template v-if="client.enable && isClientOnline(client.email)">
|
||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>{{ i18n "offline" }}</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="client" slot-scope="text, client">
|
||||
<a-space direction="horizontal" :size="2">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<template v-if="isClientDepleted(record, client.email)">{{ i18n "depleted" }}</template>
|
||||
<template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
|
||||
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
|
||||
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
|
||||
</template>
|
||||
|
@ -90,7 +85,7 @@
|
|||
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
|
||||
</td>
|
||||
<td class="tr-table-bar" v-else-if="client.totalGB > 0">
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||
</td>
|
||||
<td v-else class="infinite-bar tr-table-bar">
|
||||
<a-progress :show-info="false" :percent="100"></a-progress>
|
||||
|
@ -103,10 +98,6 @@
|
|||
</table>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<template slot="allTime" slot-scope="text, client">
|
||||
<a-tag>[[ SizeFormatter.sizeFormat(getAllTimeClient(record, client.email)) ]]</a-tag>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, client, index">
|
||||
<template v-if="client.expiryTime !=0 && client.reset >0">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
|
@ -126,7 +117,7 @@
|
|||
<tr class="tr-table-box">
|
||||
<td class="tr-table-rt"> [[ remainedDays(client.expiryTime) ]] </td>
|
||||
<td class="infinite-bar tr-table-bar">
|
||||
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||
<a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||
</td>
|
||||
<td class="tr-table-lt">[[ client.reset + "d" ]]</td>
|
||||
</tr>
|
||||
|
@ -213,7 +204,7 @@
|
|||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||
</a-popover>
|
||||
</td>
|
||||
<td width="120px" v-else class="infinite-bar">
|
||||
|
@ -247,7 +238,7 @@
|
|||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||
<a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||
</a-popover>
|
||||
</td>
|
||||
<td width="60px">[[ client.reset + "d" ]]</td>
|
||||
|
@ -287,30 +278,4 @@
|
|||
</a-badge>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="createdAt" slot-scope="text, client, index">
|
||||
<template v-if="client.created_at">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client.created_at) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client.created_at)) ]]
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</template>
|
||||
<template slot="updatedAt" slot-scope="text, client, index">
|
||||
<template v-if="client.updated_at">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client.updated_at) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client.updated_at)) ]]
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</template>
|
||||
{{end}}
|
||||
|
|
15
web/html/form/allocate.html
Normal file
15
web/html/form/allocate.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{{define "form/allocate"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='Strategy'>
|
||||
<a-select v-model="inbound.allocate.strategy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['always','random']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Refresh'>
|
||||
<a-input-number v-model.number="inbound.allocate.refresh" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Concurrency'>
|
||||
<a-input-number v-model.number="inbound.allocate.concurrency" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
|
@ -28,7 +28,7 @@
|
|||
</a-form-item>
|
||||
|
||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number v-model.number="inbound.port" :min="1" :max="65535"></a-input-number>
|
||||
<a-input-number v-model.number="inbound.port" :min="1" :max="65531"></a-input-number>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
|
@ -44,31 +44,6 @@
|
|||
<a-input-number v-model.number="dbInbound.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.periodicTrafficResetDesc" }}</span>
|
||||
<br v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
|
||||
<span v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
|
||||
<strong>{{ i18n "pages.inbounds.lastReset" }}:</strong>
|
||||
<span v-if="datepicker == 'gregorian'">[[
|
||||
moment(dbInbound.lastTrafficResetTime).format('YYYY-MM-DD HH:mm:ss') ]]</span>
|
||||
<span v-else>[[ DateUtil.convertToJalalian(moment(dbInbound.lastTrafficResetTime)) ]]</span>
|
||||
</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.periodicTrafficResetTitle" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-select v-model="dbInbound.trafficReset" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="never">{{ i18n "pages.inbounds.periodicTrafficReset.never" }}</a-select-option>
|
||||
<a-select-option value="daily">{{ i18n "pages.inbounds.periodicTrafficReset.daily" }}</a-select-option>
|
||||
<a-select-option value="weekly">{{ i18n "pages.inbounds.periodicTrafficReset.weekly" }}</a-select-option>
|
||||
<a-select-option value="monthly">{{ i18n "pages.inbounds.periodicTrafficReset.monthly" }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
|
@ -108,14 +83,14 @@
|
|||
{{template "form/shadowsocks"}}
|
||||
</template>
|
||||
|
||||
<!-- tunnel -->
|
||||
<template v-if="inbound.protocol === Protocols.TUNNEL">
|
||||
{{template "form/tunnel"}}
|
||||
<!-- dokodemo-door -->
|
||||
<template v-if="inbound.protocol === Protocols.DOKODEMO">
|
||||
{{template "form/dokodemo"}}
|
||||
</template>
|
||||
|
||||
<!-- mixed -->
|
||||
<template v-if="inbound.protocol === Protocols.MIXED">
|
||||
{{template "form/mixed"}}
|
||||
<!-- socks -->
|
||||
<template v-if="inbound.protocol === Protocols.SOCKS">
|
||||
{{template "form/socks"}}
|
||||
</template>
|
||||
|
||||
<!-- http -->
|
||||
|
@ -146,4 +121,13 @@
|
|||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
|
||||
{{end}}
|
||||
<!-- allocate -->
|
||||
<!-- Temporarily disabled until we accepts range for port allocation
|
||||
<a-collapse>
|
||||
<a-collapse-panel header='Allocate'>
|
||||
{{template "form/allocate"}}
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
-->
|
||||
|
||||
{{end}}
|
||||
|
|
|
@ -42,9 +42,6 @@
|
|||
<a-form-item label='Interval'>
|
||||
<a-input v-model.trim="outbound.settings.fragment.interval"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Split'>
|
||||
<a-input v-model.trim="outbound.settings.fragment.maxSplit"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- Switch for Noises -->
|
||||
|
@ -78,11 +75,6 @@
|
|||
<a-form-item label='Delay'>
|
||||
<a-input v-model.trim="noise.delay"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Apply To'>
|
||||
<a-select v-model="noise.applyTo" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['ip','ipv4','ipv6']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
</template>
|
||||
|
@ -105,7 +97,7 @@
|
|||
</a-form-item>
|
||||
<a-form-item label='non-IP queries'>
|
||||
<a-select v-model="outbound.settings.nonIPQuery" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[ s ]]</a-select-option>
|
||||
<a-select-option v-for="s in ['drop','skip']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="outbound.settings.nonIPQuery === 'skip'" label='Block Types' >
|
||||
|
@ -210,7 +202,7 @@
|
|||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- VLESS/VMess user settings -->
|
||||
<!-- Vnext (vless/vmess) settings -->
|
||||
<template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
|
||||
<a-form-item label='ID'>
|
||||
<a-input v-model.trim="outbound.settings.id"></a-input>
|
||||
|
@ -226,11 +218,6 @@
|
|||
</template>
|
||||
|
||||
<!-- vless settings -->
|
||||
<template v-if="outbound.protocol === Protocols.VLESS">
|
||||
<a-form-item label='encryption'>
|
||||
<a-input v-model.trim="outbound.settings.encryption"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="outbound.canEnableTlsFlow()">
|
||||
<a-form-item label='Flow'>
|
||||
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
|
@ -441,9 +428,6 @@
|
|||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="ECH Config List">
|
||||
<a-input v-model.trim="outbound.stream.tls.echConfigList"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Allow Insecure">
|
||||
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{{define "form/tunnel"}}
|
||||
{{define "form/dokodemo"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
|
||||
<a-input v-model.trim="inbound.settings.address"></a-input>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{{define "form/mixed"}}
|
||||
{{define "form/socks"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
|
||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||
|
@ -15,7 +15,7 @@
|
|||
<td width="45%">{{ i18n "username" }}</td>
|
||||
<td width="45%">{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"></a-button>
|
||||
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())"></a-button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -18,29 +18,7 @@
|
|||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="!inbound.stream.isTLS || !inbound.stream.isReality">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Authentication">
|
||||
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="X25519, not Post-Quantum">X25519 (not Post-Quantum)</a-select-option>
|
||||
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768 (Post-Quantum)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="decryption">
|
||||
<a-input v-model.trim="inbound.settings.decryption"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="encryption">
|
||||
<a-input v-model="inbound.settings.encryption"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New keys</a-button>
|
||||
<a-button danger @click="clearVlessEnc">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
|
||||
<template v-if="inbound.isTcp">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Target'>
|
||||
<a-input v-model.trim="inbound.stream.reality.target"></a-input>
|
||||
<a-form-item label='Dest (Target)'>
|
||||
<a-input v-model.trim="inbound.stream.reality.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
|
||||
|
@ -22,10 +22,10 @@
|
|||
<a-input-number v-model.number="inbound.stream.reality.maxTimediff" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Min Client Ver'>
|
||||
<a-input v-model.trim="inbound.stream.reality.minClientVer" placeholder='25.9.11'></a-input>
|
||||
<a-input v-model.trim="inbound.stream.reality.minClientVer"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Client Ver'>
|
||||
<a-input v-model.trim="inbound.stream.reality.maxClientVer" placeholder='25.9.11'></a-input>
|
||||
<a-input v-model.trim="inbound.stream.reality.maxClientVer"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
|
@ -48,10 +48,7 @@
|
|||
<a-textarea v-model="inbound.stream.reality.privateKey"></a-textarea>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
|
||||
<a-button danger @click="clearX25519Cert">Clear</a-button>
|
||||
</a-space>
|
||||
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item label="mldsa65 Seed">
|
||||
<a-textarea v-model="inbound.stream.reality.mldsa65Seed"></a-textarea>
|
||||
|
@ -60,10 +57,7 @@
|
|||
<a-textarea v-model="inbound.stream.reality.settings.mldsa65Verify"></a-textarea>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" icon="import" @click="getNewmldsa65">Get New Seed</a-button>
|
||||
<a-button danger @click="clearMldsa65">Clear</a-button>
|
||||
</a-space>
|
||||
<a-button type="primary" icon="import" @click="getNewmldsa65">Get New Seed</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
{{end}}
|
|
@ -3,14 +3,12 @@
|
|||
<a-divider :style="{ margin: '5px 0 0' }"></a-divider>
|
||||
<a-form-item label="External Proxy">
|
||||
<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-input-group :style="{ margin: '8px 0' }" compact v-for="(row, index) in inbound.stream.externalProxy">
|
||||
<template>
|
||||
<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="none">{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option value="tls">TLS</a-select-option>
|
||||
|
@ -19,7 +17,7 @@
|
|||
</template>
|
||||
<a-input :style="{ width: '30%' }" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input>
|
||||
<a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number :style="{ width: '15%' }" v-model.number="row.port" min="1" max="65535"></a-input-number>
|
||||
<a-input-number :style="{ width: '15%' }" v-model.number="row.port" min="1" max="65531"></a-input-number>
|
||||
</a-tooltip>
|
||||
<a-input :style="{ width: '30%', top: '0' }" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
|
||||
<template slot="addonAfter">
|
||||
|
@ -28,4 +26,4 @@
|
|||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
|
@ -91,6 +91,9 @@
|
|||
<a-textarea v-model="cert.key"></a-textarea>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label='OCSP stapling'>
|
||||
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="One Time Loading">
|
||||
<a-switch v-model="cert.oneTimeLoading"></a-switch>
|
||||
</a-form-item>
|
||||
|
@ -116,10 +119,7 @@
|
|||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button>
|
||||
<a-button danger @click="clearEchCert">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
1337
web/html/index.html
1337
web/html/index.html
File diff suppressed because it is too large
Load diff
|
@ -1,10 +1,456 @@
|
|||
{{ template "page/head_start" .}}
|
||||
<style>
|
||||
html * {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
/*margin: 20px 0 50px 0;*/
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.ant-form-item-children .ant-btn,
|
||||
.ant-input {
|
||||
height: 50px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.ant-input-group-addon {
|
||||
border-radius: 0 30px 30px 0;
|
||||
width: 50px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.ant-input-affix-wrapper .ant-input-prefix {
|
||||
left: 23px;
|
||||
}
|
||||
|
||||
.ant-input-affix-wrapper .ant-input:not(:first-child) {
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
margin-block-end: 2rem;
|
||||
}
|
||||
|
||||
.title b {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
#app {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#login {
|
||||
animation: charge 0.5s both;
|
||||
background-color: #fff;
|
||||
border-radius: 2rem;
|
||||
padding: 4rem 3rem;
|
||||
transition: all 0.3s;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
#login:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
||||
}
|
||||
|
||||
@keyframes charge {
|
||||
from {
|
||||
transform: translateY(5rem);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.under {
|
||||
background-color: #c7ebe2;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.dark .under {
|
||||
background-color: var(--dark-color-login-wave);
|
||||
}
|
||||
|
||||
.dark #login {
|
||||
background-color: var(--dark-color-surface-100);
|
||||
}
|
||||
|
||||
.dark h1 {
|
||||
color: rgba(255, 255, 255);
|
||||
}
|
||||
|
||||
.ant-btn-primary-login {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-btn-primary-login:focus,
|
||||
.ant-btn-primary-login:hover {
|
||||
color: #fff;
|
||||
background-color: #006655;
|
||||
border-color: #006655;
|
||||
background-image: linear-gradient(270deg,
|
||||
rgba(123, 199, 77, 0) 30%,
|
||||
#009980,
|
||||
rgba(123, 199, 77, 0) 100%);
|
||||
background-repeat: no-repeat;
|
||||
animation: ma-bg-move ease-in-out 5s infinite;
|
||||
background-position-x: -500px;
|
||||
width: 95%;
|
||||
animation-delay: -0.5s;
|
||||
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045);
|
||||
}
|
||||
|
||||
.ant-btn-primary-login.active,
|
||||
.ant-btn-primary-login:active {
|
||||
color: #fff;
|
||||
background-color: #006655;
|
||||
border-color: #006655;
|
||||
}
|
||||
|
||||
@keyframes ma-bg-move {
|
||||
0% {
|
||||
background-position: -500px 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-btn-bg {
|
||||
position: relative;
|
||||
border-radius: 25px;
|
||||
width: 100%;
|
||||
transition: all 0.3s cubic-bezier(.645, .045, .355, 1);
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg {
|
||||
color: #fff;
|
||||
position: relative;
|
||||
background-color: #0a7557;
|
||||
border: 2px double transparent;
|
||||
background-origin: border-box;
|
||||
background-clip: padding-box, border-box;
|
||||
background-size: 300%;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg:hover {
|
||||
animation: wave-btn-tara 4s ease infinite;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg-cl {
|
||||
background-image: linear-gradient(rgba(13, 14, 33, 0), rgba(13, 14, 33, 0)),
|
||||
radial-gradient(circle at left top, #006655, #009980, #006655) !important;
|
||||
border-radius: 3em;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg-cl:hover {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg-cl:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
bottom: -5px;
|
||||
right: -5px;
|
||||
z-index: -1;
|
||||
background: inherit;
|
||||
background-size: inherit;
|
||||
border-radius: 4em;
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg-cl:hover::before {
|
||||
opacity: 1;
|
||||
filter: blur(20px);
|
||||
animation: wave-btn-tara 8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes wave-btn-tara {
|
||||
to {
|
||||
background-position: 300%;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .ant-btn-primary-login {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-image: linear-gradient(rgba(13, 14, 33, 0.45),
|
||||
rgba(13, 14, 33, 0.35));
|
||||
border-radius: 2rem;
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
height: 46px;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
padding: 0 15px;
|
||||
width: 100%;
|
||||
animation: none;
|
||||
background-position-x: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.waves-header {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background-color: #dbf5ed;
|
||||
color: white;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.dark .waves-header {
|
||||
background-color: var(--dark-color-login-background);
|
||||
}
|
||||
|
||||
.waves-inner-header {
|
||||
height: 50vh;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.waves {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 15vh;
|
||||
margin-bottom: -8px;
|
||||
/*Fix for safari gap*/
|
||||
min-height: 100px;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.parallax>use {
|
||||
animation: move-forever 25s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite;
|
||||
}
|
||||
|
||||
.dark .parallax>use {
|
||||
fill: var(--dark-color-login-wave);
|
||||
}
|
||||
|
||||
.parallax>use:nth-child(1) {
|
||||
animation-delay: -2s;
|
||||
animation-duration: 4s;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.parallax>use:nth-child(2) {
|
||||
animation-delay: -3s;
|
||||
animation-duration: 7s;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.parallax>use:nth-child(3) {
|
||||
animation-delay: -4s;
|
||||
animation-duration: 10s;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.parallax>use:nth-child(4) {
|
||||
animation-delay: -5s;
|
||||
animation-duration: 13s;
|
||||
}
|
||||
|
||||
@keyframes move-forever {
|
||||
0% {
|
||||
transform: translate3d(-90px, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(85px, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.waves {
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.words-wrapper {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.words-wrapper b {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.words-wrapper b.is-visible {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.headline.zoom .words-wrapper {
|
||||
-webkit-perspective: 300px;
|
||||
-moz-perspective: 300px;
|
||||
perspective: 300px;
|
||||
}
|
||||
|
||||
.headline {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.headline.zoom b {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.headline.zoom b.is-visible {
|
||||
opacity: 1;
|
||||
-webkit-animation: zoom-in 0.8s;
|
||||
-moz-animation: zoom-in 0.8s;
|
||||
animation: cubic-bezier(0.215, 0.610, 0.355, 1.000) zoom-in 0.8s;
|
||||
}
|
||||
|
||||
.headline.zoom b.is-hidden {
|
||||
-webkit-animation: zoom-out 0.8s;
|
||||
-moz-animation: zoom-out 0.8s;
|
||||
animation: cubic-bezier(0.215, 0.610, 0.355, 1.000) zoom-out 0.4s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes zoom-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateZ(100px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateZ(0);
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes zoom-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-moz-transform: translateZ(100px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
-moz-transform: translateZ(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoom-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateZ(100px);
|
||||
-moz-transform: translateZ(100px);
|
||||
-ms-transform: translateZ(100px);
|
||||
-o-transform: translateZ(100px);
|
||||
transform: translateZ(100px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateZ(0);
|
||||
-moz-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
-o-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes zoom-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateZ(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateZ(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes zoom-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
-moz-transform: translateZ(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-moz-transform: translateZ(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoom-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateZ(0);
|
||||
-moz-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
-o-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateZ(-100px);
|
||||
-moz-transform: translateZ(-100px);
|
||||
-ms-transform: translateZ(-100px);
|
||||
-o-transform: translateZ(-100px);
|
||||
transform: translateZ(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.ant-space-item .ant-switch {
|
||||
margin: 2px 0 4px;
|
||||
}
|
||||
</style>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<transition name="list" appear>
|
||||
<a-layout-content class="under min-h-0">
|
||||
<a-layout-content class="under" :style="{ minHeight: '0' }">
|
||||
<div class="waves-header">
|
||||
<div class="waves-inner-header"></div>
|
||||
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
|
@ -20,81 +466,71 @@
|
|||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<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">
|
||||
<template v-if="!loadingStates.fetched">
|
||||
<div class="text-center">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="setting-section">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}'
|
||||
placement="bottomRight" trigger="click">
|
||||
<template slot="content">
|
||||
<a-space direction="vertical" :size="10">
|
||||
<a-theme-switch-login></a-theme-switch-login>
|
||||
<span>{{ i18n "pages.settings.language" }}</span>
|
||||
<a-select ref="selectLang" class="w-100" v-model="lang" @change="LanguageManager.setLanguage(lang)"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-button shape="circle" icon="setting"></a-button>
|
||||
</a-popover>
|
||||
</div>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col :style="{ width: '100%' }">
|
||||
<h2 class="title headline zoom">
|
||||
<span class="words-wrapper">
|
||||
<b class="is-visible">{{ i18n "pages.login.hello" }}</b>
|
||||
<b>{{ i18n "pages.login.title" }}</b>
|
||||
</span>
|
||||
</h2>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col span="24">
|
||||
<a-form @submit.prevent="login">
|
||||
<a-space direction="vertical" size="middle">
|
||||
<a-form-item>
|
||||
<a-input autocomplete="username" name="username" v-model.trim="user.username"
|
||||
placeholder='{{ i18n "username" }}' autofocus required>
|
||||
<a-icon slot="prefix" type="user" class="fs-1rem"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input-password autocomplete="current-password" name="password" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}' required>
|
||||
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="twoFactorEnable">
|
||||
<a-input autocomplete="one-time-code" name="twoFactorCode" v-model.trim="user.twoFactorCode"
|
||||
placeholder='{{ i18n "twoFactorCode" }}' required>
|
||||
<a-icon slot="prefix" type="key" class="fs-1rem"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<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'">
|
||||
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
|
||||
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
||||
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
||||
</a-button>
|
||||
</div>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
<a-row type="flex" justify="center" align="middle" :style="{ height: '100%', overflow: 'auto', overflowX: 'hidden' }">
|
||||
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" :style="{ margin: '3rem 0' }">
|
||||
<div class="setting-section">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}' placement="bottomRight" trigger="click">
|
||||
<template slot="content">
|
||||
<a-space direction="vertical" :size="10">
|
||||
<a-theme-switch-login></a-theme-switch-login>
|
||||
<span>{{ i18n "pages.settings.language" }}</span>
|
||||
<a-select ref="selectLang" :style="{ width: '100%' }" v-model="lang" @change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-button shape="circle" icon="setting"></a-button>
|
||||
</a-popover>
|
||||
</div>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col :style="{ width: '100%' }">
|
||||
<h2 class="title headline zoom">
|
||||
<span class="words-wrapper">
|
||||
<b class="is-visible">{{ i18n "pages.login.hello" }}</b>
|
||||
<b>{{ i18n "pages.login.title" }}</b>
|
||||
</span>
|
||||
</h2>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col span="24">
|
||||
<a-form>
|
||||
<a-space direction="vertical" size="middle">
|
||||
<a-form-item>
|
||||
<a-input autocomplete="username" name="username" v-model.trim="user.username"
|
||||
placeholder='{{ i18n "username" }}' @keydown.enter.native="login" autofocus>
|
||||
<a-icon slot="prefix" type="user" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input-password autocomplete="password" name="password" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
|
||||
<a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="twoFactorEnable">
|
||||
<a-input autocomplete="totp" name="twoFactorCode" v-model.trim="user.twoFactorCode"
|
||||
placeholder='{{ i18n "twoFactorCode" }}' @keydown.enter.native="login">
|
||||
<a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<div :style="{ height: '50px', marginTop: '1rem', ...loading ? { width: '52px' } : { display: 'inline-block' } }" class="wave-btn-bg wave-btn-bg-cl">
|
||||
<a-button class="ant-btn-primary-login" type="primary" :loading="loading" @click="login"
|
||||
:icon="loading ? 'poweroff' : undefined">
|
||||
[[ loading ? '' : '{{ i18n "login" }}' ]]
|
||||
</a-button>
|
||||
</div>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-layout-content>
|
||||
|
@ -108,11 +544,14 @@
|
|||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher,
|
||||
loadingStates: { fetched: false, spinning: false },
|
||||
user: { username: "", password: "", twoFactorCode: "" },
|
||||
loading: false,
|
||||
user: {
|
||||
username: "",
|
||||
password: "",
|
||||
twoFactorCode: ""
|
||||
},
|
||||
twoFactorEnable: false,
|
||||
lang: "",
|
||||
animationStarted: false
|
||||
lang: ""
|
||||
},
|
||||
async mounted() {
|
||||
this.lang = LanguageManager.getLanguage();
|
||||
|
@ -120,126 +559,60 @@
|
|||
},
|
||||
methods: {
|
||||
async login() {
|
||||
this.loadingStates.spinning = true;
|
||||
this.loading = true;
|
||||
const msg = await HttpUtil.post('/login', this.user);
|
||||
this.loading = false;
|
||||
if (msg.success) {
|
||||
location.href = basePath + 'panel/';
|
||||
}
|
||||
this.loadingStates.spinning = false;
|
||||
},
|
||||
async getTwoFactorEnable() {
|
||||
this.loading = true;
|
||||
const msg = await HttpUtil.post('/getTwoFactorEnable');
|
||||
this.loading = false;
|
||||
if (msg.success) {
|
||||
this.twoFactorEnable = msg.obj;
|
||||
this.loadingStates.fetched = true;
|
||||
this.$nextTick(() => {
|
||||
if (!this.animationStarted) {
|
||||
this.animationStarted = true;
|
||||
this.initHeadline();
|
||||
}
|
||||
});
|
||||
return msg.obj;
|
||||
}
|
||||
},
|
||||
initHeadline() {
|
||||
const animationDelay = 2000;
|
||||
const headlines = this.$el.querySelectorAll('.headline');
|
||||
headlines.forEach((headline) => {
|
||||
const first = headline.querySelector('.is-visible');
|
||||
if (!first) return;
|
||||
setTimeout(() => this.hideWord(first, animationDelay), animationDelay);
|
||||
});
|
||||
},
|
||||
hideWord(word, delay) {
|
||||
const nextWord = this.takeNext(word);
|
||||
this.switchWord(word, nextWord);
|
||||
setTimeout(() => this.hideWord(nextWord, delay), delay);
|
||||
},
|
||||
takeNext(word) {
|
||||
return word.nextElementSibling || word.parentElement.firstElementChild;
|
||||
},
|
||||
switchWord(oldWord, newWord) {
|
||||
oldWord.classList.remove('is-visible');
|
||||
oldWord.classList.add('is-hidden');
|
||||
newWord.classList.remove('is-hidden');
|
||||
newWord.classList.add('is-visible');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const pm_input_selector = 'input.ant-input, textarea.ant-input';
|
||||
const pm_strip_props = [
|
||||
'background',
|
||||
'background-color',
|
||||
'background-image',
|
||||
'color'
|
||||
];
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var animationDelay = 2000;
|
||||
initHeadline();
|
||||
|
||||
const pm_observed_forms = new WeakSet();
|
||||
|
||||
function pm_strip_inline(el) {
|
||||
if (!el || el.nodeType !== 1 || !el.matches?.(pm_input_selector)) return;
|
||||
|
||||
let did_change = false;
|
||||
for (const prop of pm_strip_props) {
|
||||
if (el.style.getPropertyValue(prop)) {
|
||||
el.style.removeProperty(prop);
|
||||
did_change = true;
|
||||
}
|
||||
function initHeadline() {
|
||||
animateHeadline(document.querySelectorAll('.headline'));
|
||||
}
|
||||
|
||||
if (did_change && el.style.length === 0) {
|
||||
el.removeAttribute('style');
|
||||
function animateHeadline(headlines) {
|
||||
var duration = animationDelay;
|
||||
headlines.forEach(function (headline) {
|
||||
setTimeout(function () {
|
||||
hideWord(headline.querySelector('.is-visible'));
|
||||
}, duration);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function pm_attach_observer(form) {
|
||||
if (pm_observed_forms.has(form)) return;
|
||||
pm_observed_forms.add(form);
|
||||
function hideWord(word) {
|
||||
var nextWord = takeNext(word);
|
||||
switchWord(word, nextWord);
|
||||
setTimeout(function () {
|
||||
hideWord(nextWord);
|
||||
}, animationDelay);
|
||||
}
|
||||
|
||||
form.querySelectorAll(pm_input_selector).forEach(pm_strip_inline);
|
||||
function takeNext(word) {
|
||||
return word.nextElementSibling ? word.nextElementSibling : word.parentElement.firstElementChild;
|
||||
}
|
||||
|
||||
const pm_mo = new MutationObserver(mutations => {
|
||||
for (const m of mutations) {
|
||||
if (m.type === 'attributes') {
|
||||
pm_strip_inline(m.target);
|
||||
} else if (m.type === 'childList') {
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType !== 1) continue;
|
||||
if (n.matches?.(pm_input_selector)) pm_strip_inline(n);
|
||||
n.querySelectorAll?.(pm_input_selector).forEach(pm_strip_inline);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
pm_mo.observe(form, {
|
||||
attributes: true,
|
||||
attributeFilter: ['style'],
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
function pm_init() {
|
||||
document.querySelectorAll('form.ant-form').forEach(pm_attach_observer);
|
||||
const pm_host = document.getElementById('login') || document.body;
|
||||
const pm_wait_for_forms = new MutationObserver(mutations => {
|
||||
for (const m of mutations) {
|
||||
for (const n of m.addedNodes) {
|
||||
if (n.nodeType !== 1) continue;
|
||||
if (n.matches?.('form.ant-form')) pm_attach_observer(n);
|
||||
n.querySelectorAll?.('form.ant-form').forEach(pm_attach_observer);
|
||||
}
|
||||
}
|
||||
});
|
||||
pm_wait_for_forms.observe(pm_host, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', pm_init, { once: true });
|
||||
} else {
|
||||
pm_init();
|
||||
}
|
||||
function switchWord(oldWord, newWord) {
|
||||
oldWord.classList.remove('is-visible');
|
||||
oldWord.classList.add('is-hidden');
|
||||
newWord.classList.remove('is-hidden');
|
||||
newWord.classList.add('is-visible');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{ template "page/body_end" .}}
|
||||
{{ template "page/body_end" .}}
|
|
@ -121,7 +121,7 @@
|
|||
},
|
||||
methods: {
|
||||
async getDBClientIps(email) {
|
||||
const msg = await HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`);
|
||||
const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`);
|
||||
if (!msg.success) {
|
||||
document.getElementById("clientIPs").value = msg.obj;
|
||||
return;
|
||||
|
@ -139,7 +139,7 @@
|
|||
},
|
||||
async clearDBClientIps(email) {
|
||||
try {
|
||||
const msg = await HttpUtil.post(`/panel/api/inbounds/clearClientIps/${email}`);
|
||||
const msg = await HttpUtil.post(`/panel/inbound/clearClientIps/${email}`);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
|
@ -156,7 +156,7 @@
|
|||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: async () => {
|
||||
iconElement.disabled = true;
|
||||
const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + email);
|
||||
const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email);
|
||||
if (msg.success) {
|
||||
this.clientModal.clientStats.up = 0;
|
||||
this.clientModal.clientStats.down = 0;
|
||||
|
|
|
@ -3,29 +3,22 @@
|
|||
:mask-closable="false" :footer="null" :class="themeSwitcher.currentTheme">
|
||||
<a-list class="ant-dns-presets-list" bordered :style="{ width: '100%' }">
|
||||
<a-list-item v-for="dns in dnsPresetsDatabase" :style="{ padding: '12px 16px' }">
|
||||
<div class="ant-dns-presets-line">
|
||||
<a-space direction="horizontal" size="small" align="center">
|
||||
<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>
|
||||
</a-space>
|
||||
<a-button class="ant-dns-presets-install" type="primary" @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
|
||||
</div>
|
||||
<a-row justify="space-between" align="middle">
|
||||
<a-col :span="12">
|
||||
<a-space direction="vertical" size="small">
|
||||
<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-col>
|
||||
<a-col :span="12" :style="{ textAlign: 'right' }">
|
||||
<a-button type="primary" @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-modal>
|
||||
|
||||
<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 {
|
||||
border-color: var(--dark-color-stroke)
|
||||
}
|
||||
|
|
|
@ -101,19 +101,9 @@
|
|||
{{ i18n "security" }}
|
||||
<a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag>
|
||||
<br />
|
||||
<td>Authentication</td>
|
||||
<a-tag v-if="inbound.settings.selectedAuth" color="green">[[ inbound.settings.selectedAuth ? inbound.settings.selectedAuth : '' ]]</a-tag>
|
||||
<a-tag v-else color="red">{{ i18n "none" }}</a-tag>
|
||||
<br />
|
||||
{{ i18n "encryption" }}
|
||||
<a-tag class="info-large-tag" :color="inbound.settings.encryption ? 'green' : 'red'">[[ inbound.settings.encryption ? inbound.settings.encryption : '' ]]</a-tag>
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<a-button size="small" icon="snippets" @click="copy(inbound.settings.encryption)"></a-button>
|
||||
</a-tooltip>
|
||||
<br />
|
||||
<template v-if="inbound.stream.security != 'none'">
|
||||
{{ i18n "domainName" }}
|
||||
<a-tag v-if="inbound.serverName" color="green">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
<a-tag v-if="inbound.serverName" :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
<a-tag v-else color="orange">{{ i18n "none" }}</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
|
@ -183,9 +173,9 @@
|
|||
<tr>
|
||||
<td>{{ i18n "status" }}</td>
|
||||
<td>
|
||||
<a-tag v-if="isDepleted" color="red">{{ i18n "depleted" }}</a-tag>
|
||||
<a-tag v-else-if="isEnable" color="green">{{ i18n "enabled" }}</a-tag>
|
||||
<a-tag v-if="isEnable" color="green">{{ i18n "enabled" }}</a-tag>
|
||||
<a-tag v-else>{{ i18n "disabled" }}</a-tag>
|
||||
<a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientStats">
|
||||
|
@ -195,44 +185,6 @@
|
|||
<a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up) ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.createdAt" }}</td>
|
||||
<td>
|
||||
<template v-if="infoModal.clientSettings && infoModal.clientSettings.created_at">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.created_at) ]]</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.created_at)) ]]</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>-</a-tag>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.updatedAt" }}</td>
|
||||
<td>
|
||||
<template v-if="infoModal.clientSettings && infoModal.clientSettings.updated_at">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.updated_at) ]]</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.updated_at)) ]]</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>-</a-tag>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "lastOnline" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ app.formatLastOnline(infoModal.clientSettings && infoModal.clientSettings.email ? infoModal.clientSettings.email : '') ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.comment">
|
||||
<td>{{ i18n "comment" }}</td>
|
||||
<td>
|
||||
|
@ -311,7 +263,7 @@
|
|||
</tr-info-title>
|
||||
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
|
||||
</tr-info-row>
|
||||
<tr-info-row class="tr-info-row" v-if="app.subSettings.subJsonEnable">
|
||||
<tr-info-row class="tr-info-row">
|
||||
<tr-info-title class="tr-info-title">
|
||||
<a-tag color="purple">Json Link</a-tag>
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
|
@ -358,7 +310,7 @@
|
|||
<code>[[ link.link ]]</code>
|
||||
</tr-info-row>
|
||||
</template>
|
||||
<table v-if="inbound.protocol == Protocols.TUNNEL" class="tr-info-table">
|
||||
<table v-if="inbound.protocol == Protocols.DOKODEMO" class="tr-info-table">
|
||||
<tr>
|
||||
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
|
||||
|
@ -380,7 +332,7 @@
|
|||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-if="dbInbound.isMixed" class="tr-info-table">
|
||||
<table v-if="dbInbound.isSocks" class="tr-info-table">
|
||||
<tr>
|
||||
<th>{{ i18n "password" }} Auth</th>
|
||||
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
|
||||
|
@ -496,7 +448,7 @@
|
|||
</a-modal>
|
||||
<script>
|
||||
function refreshIPs(email) {
|
||||
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
|
||||
return HttpUtil.post(`/panel/inbound/clientIps/${email}`).then((msg) => {
|
||||
if (msg.success) {
|
||||
try {
|
||||
return JSON.parse(msg.obj).join(', ');
|
||||
|
@ -527,7 +479,7 @@
|
|||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
|
||||
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;
|
||||
this.clientStats = this.inbound.clients ? (this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) || null) : null;
|
||||
this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
||||
|
||||
if (
|
||||
[
|
||||
|
@ -551,7 +503,7 @@
|
|||
if (this.clientSettings) {
|
||||
if (this.clientSettings.subId) {
|
||||
this.subLink = this.genSubLink(this.clientSettings.subId);
|
||||
this.subJsonLink = app.subSettings.subJsonEnable ? this.genSubJsonLink(this.clientSettings.subId) : '';
|
||||
this.subJsonLink = this.genSubJsonLink(this.clientSettings.subId);
|
||||
}
|
||||
}
|
||||
this.visible = true;
|
||||
|
@ -590,24 +542,6 @@
|
|||
}
|
||||
return infoModal.dbInbound.isEnable;
|
||||
},
|
||||
get isDepleted() {
|
||||
const stats = infoModal.clientStats;
|
||||
const settings = infoModal.clientSettings;
|
||||
if (!stats || !settings) {
|
||||
return false;
|
||||
}
|
||||
const total = stats.total ?? 0;
|
||||
const used = (stats.up ?? 0) + (stats.down ?? 0);
|
||||
const hasTotal = total > 0;
|
||||
const exhausted = hasTotal && used >= total;
|
||||
|
||||
const expiryTime = settings.expiryTime ?? 0;
|
||||
const hasExpiry = expiryTime > 0;
|
||||
const now = Date.now();
|
||||
const expired = hasExpiry && now >= expiryTime;
|
||||
|
||||
return expired || exhausted;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copy(content) {
|
||||
|
@ -635,7 +569,7 @@
|
|||
});
|
||||
},
|
||||
clearClientIps() {
|
||||
HttpUtil.post(`/panel/api/inbounds/clearClientIps/${this.infoModal.clientStats.email}`)
|
||||
HttpUtil.post(`/panel/inbound/clearClientIps/${this.infoModal.clientStats.email}`)
|
||||
.then((msg) => {
|
||||
if (!msg.success) {
|
||||
return;
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
{{define "modals/inboundModal"}}
|
||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" :dialog-style="{ top: '20px' }"
|
||||
@ok="inModal.ok" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="themeSwitcher.currentTheme" :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title"
|
||||
:dialog-style="{ top: '20px' }" @ok="inModal.ok"
|
||||
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
{{template "form/inbound"}}
|
||||
</a-modal>
|
||||
<script>
|
||||
|
@ -18,7 +20,7 @@
|
|||
ok() {
|
||||
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
|
||||
},
|
||||
show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => { }, isEdit = false }) {
|
||||
show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => {}, isEdit = false }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
if (inbound) {
|
||||
|
@ -39,7 +41,7 @@
|
|||
inModal.visible = false;
|
||||
inModal.loading(false);
|
||||
},
|
||||
loading(loading = true) {
|
||||
loading(loading=true) {
|
||||
inModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
@ -103,9 +105,9 @@
|
|||
},
|
||||
SSMethodChange() {
|
||||
this.inModal.inbound.settings.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
|
||||
|
||||
|
||||
if (this.inModal.inbound.isSSMultiUser) {
|
||||
if (this.inModal.inbound.settings.shadowsockses.length == 0) {
|
||||
if (this.inModal.inbound.settings.shadowsockses.length ==0){
|
||||
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
|
||||
}
|
||||
if (!this.inModal.inbound.isSS2022) {
|
||||
|
@ -121,7 +123,7 @@
|
|||
client.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
|
||||
})
|
||||
} else {
|
||||
if (this.inModal.inbound.settings.shadowsockses.length > 0) {
|
||||
if (this.inModal.inbound.settings.shadowsockses.length > 0){
|
||||
this.inModal.inbound.settings.shadowsockses = [];
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +134,7 @@
|
|||
},
|
||||
async getNewX25519Cert() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
|
||||
const msg = await HttpUtil.post('/server/getNewX25519Cert');
|
||||
inModal.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
|
@ -140,13 +142,9 @@
|
|||
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
|
||||
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
|
||||
},
|
||||
clearX25519Cert() {
|
||||
this.inbound.stream.reality.privateKey = '';
|
||||
this.inbound.stream.reality.settings.publicKey = '';
|
||||
},
|
||||
async getNewmldsa65() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
|
||||
const msg = await HttpUtil.post('/server/getNewmldsa65');
|
||||
inModal.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
|
@ -154,13 +152,9 @@
|
|||
inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed;
|
||||
inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify;
|
||||
},
|
||||
clearMldsa65() {
|
||||
this.inbound.stream.reality.mldsa65Seed = '';
|
||||
this.inbound.stream.reality.settings.mldsa65Verify = '';
|
||||
},
|
||||
async getNewEchCert() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni });
|
||||
const msg = await HttpUtil.post('/server/getNewEchCert', {sni: inModal.inbound.stream.tls.sni});
|
||||
inModal.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
|
@ -168,39 +162,8 @@
|
|||
inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys;
|
||||
inModal.inbound.stream.tls.settings.echConfigList = msg.obj.echConfigList;
|
||||
},
|
||||
clearEchCert() {
|
||||
this.inbound.stream.tls.echServerKeys = '';
|
||||
this.inbound.stream.tls.settings.echConfigList = '';
|
||||
},
|
||||
async getNewVlessEnc() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
|
||||
inModal.loading(false);
|
||||
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auths = msg.obj.auths || [];
|
||||
const selected = inModal.inbound.settings.selectedAuth;
|
||||
const block = auths.find(a => a.label === selected);
|
||||
|
||||
if (!block) {
|
||||
console.error("No auth block for", selected);
|
||||
return;
|
||||
}
|
||||
|
||||
inModal.inbound.settings.decryption = block.decryption;
|
||||
inModal.inbound.settings.encryption = block.encryption;
|
||||
},
|
||||
clearVlessEnc() {
|
||||
this.inbound.settings.decryption = 'none';
|
||||
this.inbound.settings.encryption = 'none';
|
||||
this.inbound.settings.selectedAuth = undefined;
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
</tr-qr-bg-inner>
|
||||
</tr-qr-bg>
|
||||
</tr-qr-box>
|
||||
<tr-qr-box class="qr-box" v-if="app.subSettings.subJsonEnable">
|
||||
<tr-qr-box class="qr-box">
|
||||
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}} Json</span></a-tag>
|
||||
<tr-qr-bg class="qr-bg-sub">
|
||||
<tr-qr-bg-inner class="qr-bg-sub-inner">
|
||||
|
@ -151,7 +151,7 @@
|
|||
methods: {
|
||||
async getStatus() {
|
||||
try {
|
||||
const msg = await HttpUtil.get('/panel/api/server/status');
|
||||
const msg = await HttpUtil.post('/server/status');
|
||||
if (msg.success) {
|
||||
this.serverStatus = msg.obj;
|
||||
}
|
||||
|
@ -262,9 +262,7 @@
|
|||
if (qrModal.client && qrModal.client.subId) {
|
||||
qrModal.subId = qrModal.client.subId;
|
||||
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
|
||||
if (app.subSettings.subJsonEnable) {
|
||||
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
|
||||
}
|
||||
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
|
||||
}
|
||||
qrModal.qrcodes.forEach((element, index) => {
|
||||
this.setQrCode("qrCode-" + index, element.link);
|
||||
|
|
|
@ -7,13 +7,12 @@
|
|||
<a-input v-model.trim="dnsModal.dnsServer.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number v-model.number="dnsModal.dnsServer.port" :min="1" :max="65535"></a-input-number>
|
||||
<a-input-number v-model.number="dnsModal.dnsServer.port" :min="1" :max="65531"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.dns.strategy" }}'>
|
||||
<a-select v-model="dnsModal.dnsServer.queryStrategy" :style="{ width: '100%' }"
|
||||
: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-form-item>
|
||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||
|
@ -76,7 +75,7 @@
|
|||
isEdit: false,
|
||||
confirm: null,
|
||||
dnsServer: { ...defaultDnsObject },
|
||||
ok() {
|
||||
ok() {
|
||||
ObjectUtil.execute(dnsModal.confirm, { ...dnsModal.dnsServer });
|
||||
},
|
||||
show({
|
||||
|
@ -107,7 +106,7 @@
|
|||
}
|
||||
} else {
|
||||
this.dnsServer = { ...defaultDnsObject };
|
||||
|
||||
|
||||
this.dnsServer.domains = [];
|
||||
this.dnsServer.expectIPs = [];
|
||||
this.dnsServer.unexpectedIPs = [];
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
{{define "modals/ruleModal"}}
|
||||
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok" :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='Domain Matcher'>
|
||||
<a-select v-model="ruleModal.rule.domainMatcher" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="dm in ['','hybrid','linear']" :value="dm">[[ dm ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
|
@ -9,7 +14,7 @@
|
|||
</template> Source IPs <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.sourceIP" placeholder="e.g. 0.0.0.0/8, fc00::/7, geoip:ir"></a-input>
|
||||
<a-input v-model.trim="ruleModal.rule.source"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
|
@ -19,17 +24,7 @@
|
|||
</template> Source Port <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.sourcePort" placeholder="e.g. 53,443,1000-2000"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template> VLESS Route <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.vlessRoute" placeholder="e.g. 53,443,1000-2000"></a-input>
|
||||
<a-input v-model.trim="ruleModal.rule.sourcePort"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Network'>
|
||||
<a-select v-model="ruleModal.rule.network" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
|
@ -62,7 +57,7 @@
|
|||
</template> IP <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.ip" placeholder="e.g. 0.0.0.0/8, fc00::/7, geoip:ir"></a-input>
|
||||
<a-input v-model.trim="ruleModal.rule.ip"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
|
@ -72,7 +67,7 @@
|
|||
</template> Domain <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.domain" placeholder="e.g. google.com, geosite:cn"></a-input>
|
||||
<a-input v-model.trim="ruleModal.rule.domain"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
|
@ -82,7 +77,7 @@
|
|||
</template> User <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.user" placeholder="e.g. email address"></a-input>
|
||||
<a-input v-model.trim="ruleModal.rule.user"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
|
@ -92,7 +87,7 @@
|
|||
</template> Port <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.port" placeholder="e.g. 53,443,1000-2000"></a-input>
|
||||
<a-input v-model.trim="ruleModal.rule.port"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Inbound Tags'>
|
||||
<a-select v-model="ruleModal.rule.inboundTag" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
|
@ -128,13 +123,13 @@
|
|||
confirm: null,
|
||||
rule: {
|
||||
type: "field",
|
||||
domainMatcher: "",
|
||||
domain: "",
|
||||
ip: "",
|
||||
port: "",
|
||||
sourcePort: "",
|
||||
vlessRoute: "",
|
||||
network: "",
|
||||
sourceIP: "",
|
||||
source: "",
|
||||
user: "",
|
||||
inboundTag: [],
|
||||
protocol: [],
|
||||
|
@ -162,13 +157,13 @@
|
|||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
if (isEdit) {
|
||||
this.rule.domainMatcher = rule.domainMatcher;
|
||||
this.rule.domain = rule.domain ? rule.domain.join(',') : [];
|
||||
this.rule.ip = rule.ip ? rule.ip.join(',') : [];
|
||||
this.rule.port = rule.port;
|
||||
this.rule.sourcePort = rule.sourcePort;
|
||||
this.rule.vlessRoute = rule.vlessRoute;
|
||||
this.rule.network = rule.network;
|
||||
this.rule.sourceIP = rule.sourceIP ? rule.sourceIP.join(',') : [];
|
||||
this.rule.source = rule.source ? rule.source.join(',') : [];
|
||||
this.rule.user = rule.user ? rule.user.join(',') : [];
|
||||
this.rule.inboundTag = rule.inboundTag;
|
||||
this.rule.protocol = rule.protocol;
|
||||
|
@ -177,13 +172,13 @@
|
|||
this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : "";
|
||||
} else {
|
||||
this.rule = {
|
||||
domainMatcher: "",
|
||||
domain: "",
|
||||
ip: "",
|
||||
port: "",
|
||||
sourcePort: "",
|
||||
vlessRoute: "",
|
||||
network: "",
|
||||
sourceIP: "",
|
||||
source: "",
|
||||
user: "",
|
||||
inboundTag: [],
|
||||
protocol: [],
|
||||
|
@ -219,13 +214,13 @@
|
|||
rule = {};
|
||||
newRule = {};
|
||||
rule.type = "field";
|
||||
rule.domainMatcher = value.domainMatcher;
|
||||
rule.domain = value.domain.length > 0 ? value.domain.split(',') : [];
|
||||
rule.ip = value.ip.length > 0 ? value.ip.split(',') : [];
|
||||
rule.port = value.port;
|
||||
rule.sourcePort = value.sourcePort;
|
||||
rule.vlessRoute = value.vlessRoute;
|
||||
rule.network = value.network;
|
||||
rule.sourceIP = value.sourceIP.length > 0 ? value.sourceIP.split(',') : [];
|
||||
rule.source = value.source.length > 0 ? value.source.split(',') : [];
|
||||
rule.user = value.user.length > 0 ? value.user.split(',') : [];
|
||||
rule.inboundTag = value.inboundTag;
|
||||
rule.protocol = value.protocol;
|
||||
|
|
|
@ -1,8 +1,67 @@
|
|||
{{ template "page/head_start" .}}
|
||||
<style>
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.ant-tabs-nav .ant-tabs-tab {
|
||||
margin: 0;
|
||||
padding: 12px .5rem;
|
||||
}
|
||||
}
|
||||
.ant-tabs-bar {
|
||||
margin: 0;
|
||||
}
|
||||
.ant-list-item {
|
||||
display: block;
|
||||
}
|
||||
.alert-msg {
|
||||
color: rgb(194, 117, 18);
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
padding: .5rem 1rem;
|
||||
text-align: center;
|
||||
background: rgb(255 145 0 / 15%);
|
||||
margin: 1.5rem 2.5rem 0rem;
|
||||
border-radius: .5rem;
|
||||
transition: all 0.5s;
|
||||
animation: signal 3s cubic-bezier(0.18, 0.89, 0.32, 1.28) infinite;
|
||||
}
|
||||
.alert-msg:hover {
|
||||
cursor: default;
|
||||
transition-duration: .3s;
|
||||
animation: signal 0.9s ease infinite;
|
||||
}
|
||||
@keyframes signal {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(194, 118, 18, 0.5);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
.alert-msg>i {
|
||||
color: inherit;
|
||||
font-size: 24px;
|
||||
}
|
||||
.dark .ant-input-password-icon {
|
||||
color: var(--dark-color-text-primary);
|
||||
}
|
||||
.ant-collapse-content-box .ant-alert {
|
||||
margin-block-end: 12px;
|
||||
}
|
||||
</style>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' settings-page'">
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<a-sidebar></a-sidebar>
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
|
@ -79,7 +138,7 @@
|
|||
</template>
|
||||
{{ template "settings/panel/subscription/general" . }}
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="5" v-if="allSetting.subJsonEnable" :style="{ paddingTop: '20px' }">
|
||||
<a-tab-pane key="5" v-if="allSetting.subEnable" :style="{ paddingTop: '20px' }">
|
||||
<template #tab>
|
||||
<a-icon type="code"></a-icon>
|
||||
<span>{{ i18n "pages.settings.subSettings" }} (JSON)</span>
|
||||
|
@ -119,6 +178,7 @@
|
|||
saveBtnDisable: true,
|
||||
user: {},
|
||||
lang: LanguageManager.getLanguage(),
|
||||
inboundOptions: [],
|
||||
remarkModels: { i: 'Inbound', e: 'Email', o: 'Other' },
|
||||
remarkSeparators: [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'],
|
||||
datepickerList: [{ name: 'Gregorian (Standard)', value: 'gregorian' }, { name: 'Jalalian (شمسی)', value: 'jalalian' }],
|
||||
|
@ -148,7 +208,7 @@
|
|||
settings: {
|
||||
domainStrategy: "AsIs",
|
||||
noises: [
|
||||
{ type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" },
|
||||
{ type: "rand", packet: "10-20", delay: "10-16" },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -242,6 +302,17 @@
|
|||
this.saveBtnDisable = true;
|
||||
}
|
||||
},
|
||||
async loadInboundTags() {
|
||||
const msg = await HttpUtil.post("/panel/inbound/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() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/panel/setting/update", this.allSetting);
|
||||
|
@ -338,7 +409,7 @@
|
|||
}
|
||||
},
|
||||
addNoise() {
|
||||
const newNoise = { type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" };
|
||||
const newNoise = { type: "rand", packet: "10-20", delay: "10-16" };
|
||||
this.noisesArray = [...this.noisesArray, newNoise];
|
||||
},
|
||||
removeNoise(index) {
|
||||
|
@ -361,13 +432,17 @@
|
|||
updatedNoises[index] = { ...updatedNoises[index], delay: value };
|
||||
this.noisesArray = updatedNoises;
|
||||
},
|
||||
updateNoiseApplyTo(index, value) {
|
||||
const updatedNoises = [...this.noisesArray];
|
||||
updatedNoises[index] = { ...updatedNoises[index], applyTo: value };
|
||||
this.noisesArray = updatedNoises;
|
||||
},
|
||||
},
|
||||
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: {
|
||||
get: function () { return this.allSetting?.subJsonFragment != ""; },
|
||||
set: function (v) {
|
||||
|
@ -523,8 +598,6 @@
|
|||
if (this.allSetting.subEnable) {
|
||||
subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath;
|
||||
if (subPath == '/sub/') alerts.push('{{ i18n "secAlertSubURI" }}');
|
||||
}
|
||||
if (this.allSetting.subJsonEnable) {
|
||||
subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;
|
||||
if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}');
|
||||
}
|
||||
|
@ -534,6 +607,7 @@
|
|||
},
|
||||
async mounted() {
|
||||
await this.getAllSetting();
|
||||
await this.loadInboundTags();
|
||||
|
||||
while (true) {
|
||||
await PromiseUtil.sleep(1000);
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
<template #title>{{ i18n "pages.settings.panelPort"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.panelPortDesc"}}</template>
|
||||
<template #control>
|
||||
<a-input-number :min="1" :min="65535" v-model="allSetting.webPort" :style="{ width: '100%' }"></a-input>
|
||||
<a-input-number :min="1" :min="65531" v-model="allSetting.webPort" :style="{ width: '100%' }"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
|
@ -137,8 +137,7 @@
|
|||
<template #title>{{ i18n "pages.settings.datepicker"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.datepickerDescription"}}</template>
|
||||
<template #control>
|
||||
<a-select :style="{ width: '100%' }" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="datepicker">
|
||||
<a-select :style="{ width: '100%' }" :dropdown-class-name="themeSwitcher.currentTheme" v-model="datepicker">
|
||||
<a-select-option v-for="item in datepickerList" :value="item.value">
|
||||
<span v-text="item.name"></span>
|
||||
</a-select-option>
|
||||
|
@ -146,5 +145,135 @@
|
|||
</template>
|
||||
</a-setting-list-item>
|
||||
</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>
|
||||
{{end}}
|
|
@ -8,13 +8,6 @@
|
|||
<a-switch v-model="allSetting.subEnable"></a-switch>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>JSON Subscription</template>
|
||||
<template #description>{{ i18n "pages.settings.subJsonEnable"}}</template>
|
||||
<template #control>
|
||||
<a-switch v-model="allSetting.subJsonEnable"></a-switch>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>{{ i18n "pages.settings.subTitle"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.subTitleDesc"}}</template>
|
||||
|
@ -40,7 +33,7 @@
|
|||
<template #title>{{ i18n "pages.settings.subPort"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.subPortDesc"}}</template>
|
||||
<template #control>
|
||||
<a-input-number v-model="allSetting.subPort" :min="1" :min="65535"
|
||||
<a-input-number v-model="allSetting.subPort" :min="1" :min="65531"
|
||||
:style="{ width: '100%' }"></a-input-number>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
|
@ -48,10 +41,7 @@
|
|||
<template #title>{{ i18n "pages.settings.subPath"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
|
||||
<template #control>
|
||||
<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>
|
||||
<a-input type="text" v-model="allSetting.subPath"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
|
|
|
@ -5,13 +5,7 @@
|
|||
<template #title>{{ i18n "pages.settings.subPath"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
|
||||
<template #control>
|
||||
<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>
|
||||
<a-input type="text" v-model="allSetting.subJsonPath"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
|
@ -96,18 +90,6 @@
|
|||
placeholder="10-20"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>ApplyTo</template>
|
||||
<template #control>
|
||||
<a-select :value="noise.applyTo" :style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
@change="(value) => updateNoiseApplyTo(index, value)">
|
||||
<a-select-option :value="p" :label="p" v-for="p in ['ip', 'ipv4', 'ipv6']" :key="p">
|
||||
<span>[[ p ]]</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-space direction="horizontal" :style="{ padding: '10px 20px' }">
|
||||
<a-button v-if="noisesArray.length > 1" type="danger"
|
||||
@click="removeNoise(index)">Remove</a-button>
|
||||
|
|
|
@ -67,22 +67,18 @@
|
|||
</template>
|
||||
<template slot="info" slot-scope="text, rule, index">
|
||||
<a-popover placement="bottomRight"
|
||||
v-if="(rule.sourceIP+rule.sourcePort+rule.vlessRoute+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0"
|
||||
v-if="(rule.source+rule.sourcePort+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0"
|
||||
:overlay-class-name="themeSwitcher.currentTheme" trigger="click">
|
||||
<template slot="content">
|
||||
<table cellpadding="2" :style="{ maxWidth: '300px' }">
|
||||
<tr v-if="rule.sourceIP">
|
||||
<td>Source IP</td>
|
||||
<td><a-tag color="blue" v-for="r in rule.sourceIP.split(',')">[[ r ]]</a-tag></td>
|
||||
<tr v-if="rule.source">
|
||||
<td>Source</td>
|
||||
<td><a-tag color="blue" v-for="r in rule.source.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.sourcePort">
|
||||
<td>Source Port</td>
|
||||
<td><a-tag color="green" v-for="r in rule.sourcePort.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.vlessRoute">
|
||||
<td>VLESS Route</td>
|
||||
<td><a-tag color="geekblue" v-for="r in rule.vlessRoute.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.network">
|
||||
<td>Network</td>
|
||||
<td><a-tag color="blue" v-for="r in rule.network.split(',')">[[ r ]]</a-tag></td>
|
||||
|
|
|
@ -3,23 +3,57 @@
|
|||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
|
||||
<style>
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ant-tabs-nav .ant-tabs-tab {
|
||||
margin: 0;
|
||||
padding: 12px .5rem;
|
||||
}
|
||||
|
||||
.ant-table-thead>tr>th,
|
||||
.ant-table-tbody>tr>td {
|
||||
padding: 10px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-bar {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-list-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-list-item>li {
|
||||
padding: 10px 20px !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box .ant-alert {
|
||||
margin-block-end: 12px;
|
||||
}
|
||||
</style>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' xray-page'">
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<a-sidebar></a-sidebar>
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
||||
<transition name="list" appear>
|
||||
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
|
||||
message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
|
||||
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }" message='{{ i18n "secAlertTitle" }}'
|
||||
color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
|
||||
</a-alert>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<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-card>
|
||||
</a-row>
|
||||
|
@ -38,8 +72,7 @@
|
|||
<a-popover v-if="restartResult" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<span slot="title">{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
|
||||
<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>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-popover>
|
||||
|
@ -148,9 +181,8 @@
|
|||
{ title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } },
|
||||
{
|
||||
title: '{{ i18n "pages.xray.rules.source"}}', children: [
|
||||
{ title: 'IP', dataIndex: "sourceIP", align: 'center', width: 20, ellipsis: true },
|
||||
{ title: '{{ i18n "pages.inbounds.port" }}', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true },
|
||||
{ title: 'VLESS Route', dataIndex: 'vlessRoute', align: 'center', width: 15, ellipsis: true }]
|
||||
{ title: 'IP', dataIndex: "source", align: 'center', width: 20, ellipsis: true },
|
||||
{ title: '{{ i18n "pages.inbounds.port" }}', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true }]
|
||||
},
|
||||
{
|
||||
title: '{{ i18n "pages.inbounds.network"}}', children: [
|
||||
|
@ -319,7 +351,7 @@
|
|||
{ label: '🇨🇳 China', value: 'geosite:cn' },
|
||||
{ label: '🇨🇳 .cn', value: 'regexp:.*\\.cn$' },
|
||||
{ label: '🇷🇺 Russia', value: 'ext:geosite_RU.dat:ru-available-only-inside' },
|
||||
{ label: '🇷🇺 .ru', value: 'regexp:.*\\.ru$' },
|
||||
{ label: '🇷🇺 .ru', value: 'regexp:.*\\.ru' },
|
||||
{ label: '🇷🇺 .su', value: 'regexp:.*\\.su$' },
|
||||
{ label: '🇷🇺 .рф', value: 'regexp:.*\\.xn--p1ai$' },
|
||||
{ label: '🇻🇳 .vn', value: 'regexp:.*\\.vn$' },
|
||||
|
@ -388,7 +420,7 @@
|
|||
},
|
||||
async restartXray() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/panel/api/server/restartXrayService");
|
||||
const msg = await HttpUtil.post("server/restartXrayService");
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
await PromiseUtil.sleep(500);
|
||||
|
@ -536,10 +568,9 @@
|
|||
serverObj = null;
|
||||
switch (o.protocol) {
|
||||
case Protocols.VMess:
|
||||
case Protocols.VLESS:
|
||||
serverObj = o.settings.vnext;
|
||||
break;
|
||||
case Protocols.VLESS:
|
||||
return [o.settings?.address + ':' + o.settings?.port];
|
||||
case Protocols.HTTP:
|
||||
case Protocols.Socks:
|
||||
case Protocols.Shadowsocks:
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue