add ldap support and portable database MySQL

This commit is contained in:
Пичугин Константин 2025-09-26 12:25:08 +03:00
parent 172f2ddaa7
commit 51b5d10edf
148 changed files with 78773 additions and 7403 deletions

View file

@ -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}"

View file

@ -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" ]

View file

@ -7,13 +7,11 @@
</picture>
</p>
[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](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`
## النجوم عبر الزمن

View file

@ -7,13 +7,11 @@
</picture>
</p>
[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](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

View file

@ -7,13 +7,11 @@
</picture>
</p>
[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](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`
## ستاره‌ها در طول زمان

View file

@ -7,13 +7,11 @@
</picture>
</p>
[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](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

View file

@ -7,13 +7,11 @@
</picture>
</p>
[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](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`
## Звезды с течением времени

View file

@ -7,13 +7,11 @@
</picture>
</p>
[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](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

Binary file not shown.

View file

@ -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
}

View file

@ -1 +1 @@
2.8.4
2.6.6

View file

@ -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
}

View file

@ -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"`
}

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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 "┌───────────────────────────────────────────────────────┐

View file

@ -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
View file

@ -1,5 +1,3 @@
// Package main is the entry point for the 3x-ui web panel application.
// It initializes the database, web server, and handles command-line operations for managing the panel.
package main
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -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,

View file

@ -1,47 +1,23 @@
// Package sub provides subscription server functionality for the 3x-ui panel,
// including HTTP/HTTPS servers for serving subscription links and JSON configurations.
package sub
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
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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 {

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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
View 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
}

View file

@ -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)
}

View file

@ -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)

View file

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

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

File diff suppressed because one or more lines are too long

View 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;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -6,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() {

View file

@ -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);
}
};

View file

@ -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', }],
}],
};
}
};

View file

@ -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
}

View file

@ -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() {

View file

@ -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;
}

File diff suppressed because it is too large Load diff

View 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')
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

6
web/assets/vue/vue.esm.browser.min.js vendored Normal file

File diff suppressed because one or more lines are too long

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View 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')
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

6
web/assets/vue/vue.runtime.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View 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

View file

@ -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()
}

View file

@ -1,21 +1,17 @@
// Package controller provides HTTP request handlers and controllers for the 3x-ui web management panel.
// It handles routing, authentication, and API endpoints for managing Xray inbounds, settings, and more.
package controller
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 {

View file

@ -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()
}
}

View file

@ -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 {

View file

@ -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)
}

View file

@ -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 {

View file

@ -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"
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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)

View file

@ -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
}

View file

@ -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()

View file

@ -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}}

View 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}}

View file

@ -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}}

View file

@ -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>

View file

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

View file

@ -1,4 +1,4 @@
{{define "form/mixed"}}
{{define "form/socks"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-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>

View file

@ -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>

View file

@ -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}}

View file

@ -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}}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</a-space>
</template>
<a-button shape="circle" icon="setting"></a-button>
</a-popover>
</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>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</a-space>
</template>
<a-button shape="circle" icon="setting"></a-button>
</a-popover>
</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" .}}

View file

@ -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;

View file

@ -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)
}

View file

@ -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;

View file

@ -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}}

View file

@ -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);

View file

@ -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 = [];

View file

@ -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;

View file

@ -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);

View file

@ -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}}

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -3,23 +3,57 @@
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/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