mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-13 19:49:12 +00:00
Merge branch 'main' into feature/multi-server-support
This commit is contained in:
commit
4c7249c451
80 changed files with 1087 additions and 256 deletions
3
.github/workflows/docker.yml
vendored
3
.github/workflows/docker.yml
vendored
|
@ -1,4 +1,7 @@
|
||||||
name: Release 3X-UI for Docker
|
name: Release 3X-UI for Docker
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
|
|
|
@ -49,6 +49,7 @@ RUN chmod +x \
|
||||||
/usr/bin/x-ui
|
/usr/bin/x-ui
|
||||||
|
|
||||||
ENV XUI_ENABLE_FAIL2BAN="true"
|
ENV XUI_ENABLE_FAIL2BAN="true"
|
||||||
|
EXPOSE 2053
|
||||||
VOLUME [ "/etc/x-ui" ]
|
VOLUME [ "/etc/x-ui" ]
|
||||||
CMD [ "./x-ui" ]
|
CMD [ "./x-ui" ]
|
||||||
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]
|
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package config provides configuration management utilities for the 3x-ui panel,
|
||||||
|
// including version information, logging levels, database paths, and environment variable handling.
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -16,24 +18,29 @@ var version string
|
||||||
//go:embed name
|
//go:embed name
|
||||||
var name string
|
var name string
|
||||||
|
|
||||||
|
// LogLevel represents the logging level for the application.
|
||||||
type LogLevel string
|
type LogLevel string
|
||||||
|
|
||||||
|
// Logging level constants
|
||||||
const (
|
const (
|
||||||
Debug LogLevel = "debug"
|
Debug LogLevel = "debug"
|
||||||
Info LogLevel = "info"
|
Info LogLevel = "info"
|
||||||
Notice LogLevel = "notice"
|
Notice LogLevel = "notice"
|
||||||
Warn LogLevel = "warn"
|
Warning LogLevel = "warning"
|
||||||
Error LogLevel = "error"
|
Error LogLevel = "error"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetVersion returns the version string of the 3x-ui application.
|
||||||
func GetVersion() string {
|
func GetVersion() string {
|
||||||
return strings.TrimSpace(version)
|
return strings.TrimSpace(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the 3x-ui application.
|
||||||
func GetName() string {
|
func GetName() string {
|
||||||
return strings.TrimSpace(name)
|
return strings.TrimSpace(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLogLevel returns the current logging level based on environment variables or defaults to Info.
|
||||||
func GetLogLevel() LogLevel {
|
func GetLogLevel() LogLevel {
|
||||||
if IsDebug() {
|
if IsDebug() {
|
||||||
return Debug
|
return Debug
|
||||||
|
@ -45,10 +52,12 @@ func GetLogLevel() LogLevel {
|
||||||
return LogLevel(logLevel)
|
return LogLevel(logLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsDebug returns true if debug mode is enabled via the XUI_DEBUG environment variable.
|
||||||
func IsDebug() bool {
|
func IsDebug() bool {
|
||||||
return os.Getenv("XUI_DEBUG") == "true"
|
return os.Getenv("XUI_DEBUG") == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBinFolderPath returns the path to the binary folder, defaulting to "bin" if not set via XUI_BIN_FOLDER.
|
||||||
func GetBinFolderPath() string {
|
func GetBinFolderPath() string {
|
||||||
binFolderPath := os.Getenv("XUI_BIN_FOLDER")
|
binFolderPath := os.Getenv("XUI_BIN_FOLDER")
|
||||||
if binFolderPath == "" {
|
if binFolderPath == "" {
|
||||||
|
@ -74,6 +83,7 @@ func getBaseDir() string {
|
||||||
return exeDir
|
return exeDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDBFolderPath returns the path to the database folder based on environment variables or platform defaults.
|
||||||
func GetDBFolderPath() string {
|
func GetDBFolderPath() string {
|
||||||
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
|
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
|
||||||
if dbFolderPath != "" {
|
if dbFolderPath != "" {
|
||||||
|
@ -85,10 +95,12 @@ func GetDBFolderPath() string {
|
||||||
return "/etc/x-ui"
|
return "/etc/x-ui"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDBPath returns the full path to the database file.
|
||||||
func GetDBPath() string {
|
func GetDBPath() string {
|
||||||
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLogFolder returns the path to the log folder based on environment variables or platform defaults.
|
||||||
func GetLogFolder() string {
|
func GetLogFolder() string {
|
||||||
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
|
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
|
||||||
if logFolderPath != "" {
|
if logFolderPath != "" {
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
2.8.2
|
2.8.3
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package database provides database initialization, migration, and management utilities
|
||||||
|
// for the 3x-ui panel using GORM with SQLite.
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -46,6 +48,7 @@ func initModels() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initUser creates a default admin user if the users table is empty.
|
||||||
func initUser() error {
|
func initUser() error {
|
||||||
empty, err := isTableEmpty("users")
|
empty, err := isTableEmpty("users")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -69,6 +72,7 @@ func initUser() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runSeeders migrates user passwords to bcrypt and records seeder execution to prevent re-running.
|
||||||
func runSeeders(isUsersEmpty bool) error {
|
func runSeeders(isUsersEmpty bool) error {
|
||||||
empty, err := isTableEmpty("history_of_seeders")
|
empty, err := isTableEmpty("history_of_seeders")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -108,12 +112,14 @@ func runSeeders(isUsersEmpty bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isTableEmpty returns true if the named table contains zero rows.
|
||||||
func isTableEmpty(tableName string) (bool, error) {
|
func isTableEmpty(tableName string) (bool, error) {
|
||||||
var count int64
|
var count int64
|
||||||
err := db.Table(tableName).Count(&count).Error
|
err := db.Table(tableName).Count(&count).Error
|
||||||
return count == 0, err
|
return count == 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitDB sets up the database connection, migrates models, and runs seeders.
|
||||||
func InitDB(dbPath string) error {
|
func InitDB(dbPath string) error {
|
||||||
dir := path.Dir(dbPath)
|
dir := path.Dir(dbPath)
|
||||||
err := os.MkdirAll(dir, fs.ModePerm)
|
err := os.MkdirAll(dir, fs.ModePerm)
|
||||||
|
@ -152,6 +158,7 @@ func InitDB(dbPath string) error {
|
||||||
return runSeeders(isUsersEmpty)
|
return runSeeders(isUsersEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloseDB closes the database connection if it exists.
|
||||||
func CloseDB() error {
|
func CloseDB() error {
|
||||||
if db != nil {
|
if db != nil {
|
||||||
sqlDB, err := db.DB()
|
sqlDB, err := db.DB()
|
||||||
|
@ -163,14 +170,17 @@ func CloseDB() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDB returns the global GORM database instance.
|
||||||
func GetDB() *gorm.DB {
|
func GetDB() *gorm.DB {
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsNotFound checks if the given error is a GORM record not found error.
|
||||||
func IsNotFound(err error) bool {
|
func IsNotFound(err error) bool {
|
||||||
return err == gorm.ErrRecordNotFound
|
return err == gorm.ErrRecordNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSQLiteDB checks if the given file is a valid SQLite database by reading its signature.
|
||||||
func IsSQLiteDB(file io.ReaderAt) (bool, error) {
|
func IsSQLiteDB(file io.ReaderAt) (bool, error) {
|
||||||
signature := []byte("SQLite format 3\x00")
|
signature := []byte("SQLite format 3\x00")
|
||||||
buf := make([]byte, len(signature))
|
buf := make([]byte, len(signature))
|
||||||
|
@ -181,6 +191,7 @@ func IsSQLiteDB(file io.ReaderAt) (bool, error) {
|
||||||
return bytes.Equal(buf, signature), nil
|
return bytes.Equal(buf, signature), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checkpoint performs a WAL checkpoint on the SQLite database to ensure data consistency.
|
||||||
func Checkpoint() error {
|
func Checkpoint() error {
|
||||||
// Update WAL
|
// Update WAL
|
||||||
err := db.Exec("PRAGMA wal_checkpoint;").Error
|
err := db.Exec("PRAGMA wal_checkpoint;").Error
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Package model defines the database models and data structures used by the 3x-ui panel.
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -7,8 +8,10 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Protocol represents the protocol type for Xray inbounds.
|
||||||
type Protocol string
|
type Protocol string
|
||||||
|
|
||||||
|
// Protocol constants for different Xray inbound protocols
|
||||||
const (
|
const (
|
||||||
VMESS Protocol = "vmess"
|
VMESS Protocol = "vmess"
|
||||||
VLESS Protocol = "vless"
|
VLESS Protocol = "vless"
|
||||||
|
@ -20,27 +23,29 @@ const (
|
||||||
WireGuard Protocol = "wireguard"
|
WireGuard Protocol = "wireguard"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// User represents a user account in the 3x-ui panel.
|
||||||
type User struct {
|
type User struct {
|
||||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inbound represents an Xray inbound configuration with traffic statistics and settings.
|
||||||
type Inbound struct {
|
type Inbound struct {
|
||||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier
|
||||||
UserId int `json:"-"`
|
UserId int `json:"-"` // Associated user ID
|
||||||
Up int64 `json:"up" form:"up"`
|
Up int64 `json:"up" form:"up"` // Upload traffic in bytes
|
||||||
Down int64 `json:"down" form:"down"`
|
Down int64 `json:"down" form:"down"` // Download traffic in bytes
|
||||||
Total int64 `json:"total" form:"total"`
|
Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
|
||||||
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"`
|
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` // All-time traffic usage
|
||||||
Remark string `json:"remark" form:"remark"`
|
Remark string `json:"remark" form:"remark"` // Human-readable remark
|
||||||
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"`
|
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled
|
||||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
|
||||||
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2"`
|
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2"` // Traffic reset schedule
|
||||||
LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"`
|
LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
|
||||||
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`
|
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics
|
||||||
|
|
||||||
// config part
|
// Xray configuration fields
|
||||||
Listen string `json:"listen" form:"listen"`
|
Listen string `json:"listen" form:"listen"`
|
||||||
Port int `json:"port" form:"port"`
|
Port int `json:"port" form:"port"`
|
||||||
Protocol Protocol `json:"protocol" form:"protocol"`
|
Protocol Protocol `json:"protocol" form:"protocol"`
|
||||||
|
@ -50,6 +55,7 @@ type Inbound struct {
|
||||||
Sniffing string `json:"sniffing" form:"sniffing"`
|
Sniffing string `json:"sniffing" form:"sniffing"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OutboundTraffics tracks traffic statistics for Xray outbound connections.
|
||||||
type OutboundTraffics struct {
|
type OutboundTraffics struct {
|
||||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||||
|
@ -58,17 +64,20 @@ type OutboundTraffics struct {
|
||||||
Total int64 `json:"total" form:"total" gorm:"default:0"`
|
Total int64 `json:"total" form:"total" gorm:"default:0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InboundClientIps stores IP addresses associated with inbound clients for access control.
|
||||||
type InboundClientIps struct {
|
type InboundClientIps struct {
|
||||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
|
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
|
||||||
Ips string `json:"ips" form:"ips"`
|
Ips string `json:"ips" form:"ips"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HistoryOfSeeders tracks which database seeders have been executed to prevent re-running.
|
||||||
type HistoryOfSeeders struct {
|
type HistoryOfSeeders struct {
|
||||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
SeederName string `json:"seederName"`
|
SeederName string `json:"seederName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
|
||||||
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||||
listen := i.Listen
|
listen := i.Listen
|
||||||
if listen != "" {
|
if listen != "" {
|
||||||
|
@ -85,35 +94,30 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setting stores key-value configuration settings for the 3x-ui panel.
|
||||||
type Setting struct {
|
type Setting struct {
|
||||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
Key string `json:"key" form:"key"`
|
Key string `json:"key" form:"key"`
|
||||||
Value string `json:"value" form:"value"`
|
Value string `json:"value" form:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Client represents a client configuration for Xray inbounds with traffic limits and settings.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"` // Unique client identifier
|
||||||
Security string `json:"security"`
|
Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm")
|
||||||
Password string `json:"password"`
|
Password string `json:"password"` // Client password
|
||||||
Flow string `json:"flow"`
|
Flow string `json:"flow"` // Flow control (XTLS)
|
||||||
Email string `json:"email"`
|
Email string `json:"email"` // Client email identifier
|
||||||
LimitIP int `json:"limitIp"`
|
LimitIP int `json:"limitIp"` // IP limit for this client
|
||||||
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB
|
||||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
|
||||||
Enable bool `json:"enable" form:"enable"`
|
Enable bool `json:"enable" form:"enable"` // Whether the client is enabled
|
||||||
TgID int64 `json:"tgId" form:"tgId"`
|
TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications
|
||||||
SubID string `json:"subId" form:"subId"`
|
SubID string `json:"subId" form:"subId"` // Subscription identifier
|
||||||
Comment string `json:"comment" form:"comment"`
|
Comment string `json:"comment" form:"comment"` // Client comment
|
||||||
Reset int `json:"reset" form:"reset"`
|
Reset int `json:"reset" form:"reset"` // Reset period in days
|
||||||
CreatedAt int64 `json:"created_at,omitempty"`
|
CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
|
||||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
|
||||||
}
|
|
||||||
|
|
||||||
type VLESSSettings struct {
|
|
||||||
Clients []Client `json:"clients"`
|
|
||||||
Decryption string `json:"decryption"`
|
|
||||||
Encryption string `json:"encryption"`
|
|
||||||
Fallbacks []any `json:"fallbacks"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
|
|
14
go.mod
14
go.mod
|
@ -5,7 +5,7 @@ go 1.25.1
|
||||||
require (
|
require (
|
||||||
github.com/gin-contrib/gzip v1.2.3
|
github.com/gin-contrib/gzip v1.2.3
|
||||||
github.com/gin-contrib/sessions v1.0.4
|
github.com/gin-contrib/sessions v1.0.4
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/goccy/go-json v0.10.5
|
github.com/goccy/go-json v0.10.5
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
@ -16,7 +16,7 @@ require (
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/shirou/gopsutil/v4 v4.25.8
|
github.com/shirou/gopsutil/v4 v4.25.8
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/valyala/fasthttp v1.65.0
|
github.com/valyala/fasthttp v1.66.0
|
||||||
github.com/xlzd/gotp v0.1.0
|
github.com/xlzd/gotp v0.1.0
|
||||||
github.com/xtls/xray-core v1.250911.0
|
github.com/xtls/xray-core v1.250911.0
|
||||||
go.uber.org/atomic v1.11.0
|
go.uber.org/atomic v1.11.0
|
||||||
|
@ -25,7 +25,7 @@ require (
|
||||||
golang.org/x/text v0.29.0
|
golang.org/x/text v0.29.0
|
||||||
google.golang.org/grpc v1.75.1
|
google.golang.org/grpc v1.75.1
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
gorm.io/gorm v1.30.5
|
gorm.io/gorm v1.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -36,13 +36,14 @@ require (
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
|
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
|
||||||
github.com/ebitengine/purego v0.8.4 // indirect
|
github.com/ebitengine/purego v0.9.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
github.com/google/btree v1.1.3 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/gorilla/context v1.1.2 // indirect
|
github.com/gorilla/context v1.1.2 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
|
@ -71,7 +72,7 @@ require (
|
||||||
github.com/refraction-networking/utls v1.8.0 // indirect
|
github.com/refraction-networking/utls v1.8.0 // indirect
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/sagernet/sing v0.7.7 // indirect
|
github.com/sagernet/sing v0.7.10 // indirect
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
||||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
|
@ -92,12 +93,11 @@ require (
|
||||||
golang.org/x/net v0.44.0 // indirect
|
golang.org/x/net v0.44.0 // indirect
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
golang.org/x/time v0.13.0 // indirect
|
golang.org/x/time v0.13.0 // indirect
|
||||||
golang.org/x/tools v0.36.0 // indirect
|
golang.org/x/tools v0.37.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
||||||
lukechampine.com/blake3 v1.4.1 // indirect
|
lukechampine.com/blake3 v1.4.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
26
go.sum
26
go.sum
|
@ -19,8 +19,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||||
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
|
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
|
||||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||||
|
@ -31,8 +31,8 @@ github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kb
|
||||||
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
|
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
@ -50,6 +50,8 @@ github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHO
|
||||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
||||||
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
@ -134,8 +136,8 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/sagernet/sing v0.7.7 h1:o46FzVZS+wKbBMEkMEdEHoVZxyM9jvfRpKXc7pEgS/c=
|
github.com/sagernet/sing v0.7.10 h1:2yPhZFx+EkyHPH8hXNezgyRSHyGY12CboId7CtwLROw=
|
||||||
github.com/sagernet/sing v0.7.7/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
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 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
||||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
||||||
|
@ -166,8 +168,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
|
||||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
|
github.com/valyala/fasthttp v1.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNuyU=
|
||||||
github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
|
github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs=
|
||||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||||
|
@ -224,8 +226,8 @@ golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
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 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||||
|
@ -249,8 +251,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s=
|
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||||
gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
|
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
|
||||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
|
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
|
||||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package logger provides logging functionality for the 3x-ui panel with
|
||||||
|
// buffered log storage and multiple log levels.
|
||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -10,6 +12,10 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logger *logging.Logger
|
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.
|
||||||
logBuffer []struct {
|
logBuffer []struct {
|
||||||
time string
|
time string
|
||||||
level logging.Level
|
level logging.Level
|
||||||
|
@ -21,6 +27,7 @@ func init() {
|
||||||
InitLogger(logging.INFO)
|
InitLogger(logging.INFO)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitLogger initializes the logger with the specified logging level.
|
||||||
func InitLogger(level logging.Level) {
|
func InitLogger(level logging.Level) {
|
||||||
newLogger := logging.MustGetLogger("x-ui")
|
newLogger := logging.MustGetLogger("x-ui")
|
||||||
var err error
|
var err error
|
||||||
|
@ -47,51 +54,61 @@ func InitLogger(level logging.Level) {
|
||||||
logger = newLogger
|
logger = newLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug logs a debug message and adds it to the log buffer.
|
||||||
func Debug(args ...any) {
|
func Debug(args ...any) {
|
||||||
logger.Debug(args...)
|
logger.Debug(args...)
|
||||||
addToBuffer("DEBUG", fmt.Sprint(args...))
|
addToBuffer("DEBUG", fmt.Sprint(args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debugf logs a formatted debug message and adds it to the log buffer.
|
||||||
func Debugf(format string, args ...any) {
|
func Debugf(format string, args ...any) {
|
||||||
logger.Debugf(format, args...)
|
logger.Debugf(format, args...)
|
||||||
addToBuffer("DEBUG", fmt.Sprintf(format, args...))
|
addToBuffer("DEBUG", fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Info logs an info message and adds it to the log buffer.
|
||||||
func Info(args ...any) {
|
func Info(args ...any) {
|
||||||
logger.Info(args...)
|
logger.Info(args...)
|
||||||
addToBuffer("INFO", fmt.Sprint(args...))
|
addToBuffer("INFO", fmt.Sprint(args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Infof logs a formatted info message and adds it to the log buffer.
|
||||||
func Infof(format string, args ...any) {
|
func Infof(format string, args ...any) {
|
||||||
logger.Infof(format, args...)
|
logger.Infof(format, args...)
|
||||||
addToBuffer("INFO", fmt.Sprintf(format, args...))
|
addToBuffer("INFO", fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notice logs a notice message and adds it to the log buffer.
|
||||||
func Notice(args ...any) {
|
func Notice(args ...any) {
|
||||||
logger.Notice(args...)
|
logger.Notice(args...)
|
||||||
addToBuffer("NOTICE", fmt.Sprint(args...))
|
addToBuffer("NOTICE", fmt.Sprint(args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Noticef logs a formatted notice message and adds it to the log buffer.
|
||||||
func Noticef(format string, args ...any) {
|
func Noticef(format string, args ...any) {
|
||||||
logger.Noticef(format, args...)
|
logger.Noticef(format, args...)
|
||||||
addToBuffer("NOTICE", fmt.Sprintf(format, args...))
|
addToBuffer("NOTICE", fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Warning logs a warning message and adds it to the log buffer.
|
||||||
func Warning(args ...any) {
|
func Warning(args ...any) {
|
||||||
logger.Warning(args...)
|
logger.Warning(args...)
|
||||||
addToBuffer("WARNING", fmt.Sprint(args...))
|
addToBuffer("WARNING", fmt.Sprint(args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Warningf logs a formatted warning message and adds it to the log buffer.
|
||||||
func Warningf(format string, args ...any) {
|
func Warningf(format string, args ...any) {
|
||||||
logger.Warningf(format, args...)
|
logger.Warningf(format, args...)
|
||||||
addToBuffer("WARNING", fmt.Sprintf(format, args...))
|
addToBuffer("WARNING", fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error logs an error message and adds it to the log buffer.
|
||||||
func Error(args ...any) {
|
func Error(args ...any) {
|
||||||
logger.Error(args...)
|
logger.Error(args...)
|
||||||
addToBuffer("ERROR", fmt.Sprint(args...))
|
addToBuffer("ERROR", fmt.Sprint(args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Errorf logs a formatted error message and adds it to the log buffer.
|
||||||
func Errorf(format string, args ...any) {
|
func Errorf(format string, args ...any) {
|
||||||
logger.Errorf(format, args...)
|
logger.Errorf(format, args...)
|
||||||
addToBuffer("ERROR", fmt.Sprintf(format, args...))
|
addToBuffer("ERROR", fmt.Sprintf(format, args...))
|
||||||
|
@ -115,6 +132,7 @@ 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 {
|
func GetLogs(c int, level string) []string {
|
||||||
var output []string
|
var output []string
|
||||||
logLevel, _ := logging.LogLevel(level)
|
logLevel, _ := logging.LogLevel(level)
|
||||||
|
|
17
main.go
17
main.go
|
@ -1,3 +1,5 @@
|
||||||
|
// Package main is the entry point for the 3x-ui web panel application.
|
||||||
|
// It initializes the database, web server, and handles command-line operations for managing the panel.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -22,6 +24,7 @@ import (
|
||||||
"github.com/op/go-logging"
|
"github.com/op/go-logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// runWebServer initializes and starts the web server for the 3x-ui panel.
|
||||||
func runWebServer() {
|
func runWebServer() {
|
||||||
log.Printf("Starting %v %v", config.GetName(), config.GetVersion())
|
log.Printf("Starting %v %v", config.GetName(), config.GetVersion())
|
||||||
|
|
||||||
|
@ -32,7 +35,7 @@ func runWebServer() {
|
||||||
logger.InitLogger(logging.INFO)
|
logger.InitLogger(logging.INFO)
|
||||||
case config.Notice:
|
case config.Notice:
|
||||||
logger.InitLogger(logging.NOTICE)
|
logger.InitLogger(logging.NOTICE)
|
||||||
case config.Warn:
|
case config.Warning:
|
||||||
logger.InitLogger(logging.WARNING)
|
logger.InitLogger(logging.WARNING)
|
||||||
case config.Error:
|
case config.Error:
|
||||||
logger.InitLogger(logging.ERROR)
|
logger.InitLogger(logging.ERROR)
|
||||||
|
@ -111,6 +114,7 @@ func runWebServer() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resetSetting resets all panel settings to their default values.
|
||||||
func resetSetting() {
|
func resetSetting() {
|
||||||
err := database.InitDB(config.GetDBPath())
|
err := database.InitDB(config.GetDBPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -127,6 +131,7 @@ func resetSetting() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// showSetting displays the current panel settings if show is true.
|
||||||
func showSetting(show bool) {
|
func showSetting(show bool) {
|
||||||
if show {
|
if show {
|
||||||
settingService := service.SettingService{}
|
settingService := service.SettingService{}
|
||||||
|
@ -176,6 +181,7 @@ func showSetting(show bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateTgbotEnableSts enables or disables the Telegram bot notifications based on the status parameter.
|
||||||
func updateTgbotEnableSts(status bool) {
|
func updateTgbotEnableSts(status bool) {
|
||||||
settingService := service.SettingService{}
|
settingService := service.SettingService{}
|
||||||
currentTgSts, err := settingService.GetTgbotEnabled()
|
currentTgSts, err := settingService.GetTgbotEnabled()
|
||||||
|
@ -195,6 +201,7 @@ func updateTgbotEnableSts(status bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateTgbotSetting updates Telegram bot settings including token, chat ID, and runtime schedule.
|
||||||
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
||||||
err := database.InitDB(config.GetDBPath())
|
err := database.InitDB(config.GetDBPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -232,7 +239,9 @@ 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, apiKey string) {
|
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool, apiKey string) {
|
||||||
|
|
||||||
err := database.InitDB(config.GetDBPath())
|
err := database.InitDB(config.GetDBPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Database initialization failed:", err)
|
fmt.Println("Database initialization failed:", err)
|
||||||
|
@ -299,6 +308,7 @@ func updateSetting(port int, username string, password string, webBasePath strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateCert updates the SSL certificate files for the panel.
|
||||||
func updateCert(publicKey string, privateKey string) {
|
func updateCert(publicKey string, privateKey string) {
|
||||||
err := database.InitDB(config.GetDBPath())
|
err := database.InitDB(config.GetDBPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -326,6 +336,7 @@ func updateCert(publicKey string, privateKey string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCertificate displays the current SSL certificate settings if getCert is true.
|
||||||
func GetCertificate(getCert bool) {
|
func GetCertificate(getCert bool) {
|
||||||
if getCert {
|
if getCert {
|
||||||
settingService := service.SettingService{}
|
settingService := service.SettingService{}
|
||||||
|
@ -343,6 +354,7 @@ func GetCertificate(getCert bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetListenIP displays the current panel listen IP address if getListen is true.
|
||||||
func GetListenIP(getListen bool) {
|
func GetListenIP(getListen bool) {
|
||||||
if getListen {
|
if getListen {
|
||||||
|
|
||||||
|
@ -357,6 +369,7 @@ func GetListenIP(getListen bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// migrateDb performs database migration operations for the 3x-ui panel.
|
||||||
func migrateDb() {
|
func migrateDb() {
|
||||||
inboundService := service.InboundService{}
|
inboundService := service.InboundService{}
|
||||||
|
|
||||||
|
@ -369,6 +382,8 @@ func migrateDb() {
|
||||||
fmt.Println("Migration done!")
|
fmt.Println("Migration done!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// main is the entry point of the 3x-ui application.
|
||||||
|
// It parses command-line arguments to run the web server, migrate database, or update settings.
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
runWebServer()
|
runWebServer()
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package sub provides subscription server functionality for the 3x-ui panel,
|
||||||
|
// including HTTP/HTTPS servers for serving subscription links and JSON configurations.
|
||||||
package sub
|
package sub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -39,6 +41,7 @@ func setEmbeddedTemplates(engine *gin.Engine) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server represents the subscription server that serves subscription links and JSON configurations.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
|
@ -50,6 +53,7 @@ type Server struct {
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewServer creates a new subscription server instance with a cancellable context.
|
||||||
func NewServer() *Server {
|
func NewServer() *Server {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
return &Server{
|
return &Server{
|
||||||
|
@ -58,6 +62,8 @@ func NewServer() *Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initRouter configures the subscription server's Gin engine, middleware,
|
||||||
|
// templates and static assets and returns the ready-to-use engine.
|
||||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
// Always run in release mode for the subscription server
|
// Always run in release mode for the subscription server
|
||||||
gin.DefaultWriter = io.Discard
|
gin.DefaultWriter = io.Discard
|
||||||
|
@ -222,6 +228,7 @@ func (s *Server) getHtmlFiles() ([]string, error) {
|
||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start initializes and starts the subscription server with configured settings.
|
||||||
func (s *Server) Start() (err error) {
|
func (s *Server) Start() (err error) {
|
||||||
// This is an anonymous function, no function name
|
// This is an anonymous function, no function name
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -295,6 +302,7 @@ func (s *Server) Start() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop gracefully shuts down the subscription server and closes the listener.
|
||||||
func (s *Server) Stop() error {
|
func (s *Server) Stop() error {
|
||||||
s.cancel()
|
s.cancel()
|
||||||
|
|
||||||
|
@ -309,6 +317,7 @@ func (s *Server) Stop() error {
|
||||||
return common.Combine(err1, err2)
|
return common.Combine(err1, err2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCtx returns the server's context for cancellation and deadline management.
|
||||||
func (s *Server) GetCtx() context.Context {
|
func (s *Server) GetCtx() context.Context {
|
||||||
return s.ctx
|
return s.ctx
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SUBController handles HTTP requests for subscription links and JSON configurations.
|
||||||
type SUBController struct {
|
type SUBController struct {
|
||||||
subTitle string
|
subTitle string
|
||||||
subPath string
|
subPath string
|
||||||
|
@ -22,6 +23,7 @@ type SUBController struct {
|
||||||
subJsonService *SubJsonService
|
subJsonService *SubJsonService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSUBController creates a new subscription controller with the given configuration.
|
||||||
func NewSUBController(
|
func NewSUBController(
|
||||||
g *gin.RouterGroup,
|
g *gin.RouterGroup,
|
||||||
subPath string,
|
subPath string,
|
||||||
|
@ -53,6 +55,8 @@ func NewSUBController(
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initRouter registers HTTP routes for subscription links and JSON endpoints
|
||||||
|
// on the provided router group.
|
||||||
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||||
gLink := g.Group(a.subPath)
|
gLink := g.Group(a.subPath)
|
||||||
gLink.GET(":subid", a.subs)
|
gLink.GET(":subid", a.subs)
|
||||||
|
@ -62,6 +66,7 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// subs handles HTTP requests for subscription links, returning either HTML page or base64-encoded subscription data.
|
||||||
func (a *SUBController) subs(c *gin.Context) {
|
func (a *SUBController) subs(c *gin.Context) {
|
||||||
subId := c.Param("subid")
|
subId := c.Param("subid")
|
||||||
scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c)
|
scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c)
|
||||||
|
@ -119,6 +124,7 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// subJsons handles HTTP requests for JSON subscription configurations.
|
||||||
func (a *SUBController) subJsons(c *gin.Context) {
|
func (a *SUBController) subJsons(c *gin.Context) {
|
||||||
subId := c.Param("subid")
|
subId := c.Param("subid")
|
||||||
_, host, _, _ := a.subService.ResolveRequest(c)
|
_, host, _, _ := a.subService.ResolveRequest(c)
|
||||||
|
@ -134,6 +140,7 @@ func (a *SUBController) subJsons(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title.
|
||||||
func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
|
func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
|
||||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||||
c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
|
c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
//go:embed default.json
|
//go:embed default.json
|
||||||
var defaultJson string
|
var defaultJson string
|
||||||
|
|
||||||
|
// SubJsonService handles JSON subscription configuration generation and management.
|
||||||
type SubJsonService struct {
|
type SubJsonService struct {
|
||||||
configJson map[string]any
|
configJson map[string]any
|
||||||
defaultOutbounds []json_util.RawMessage
|
defaultOutbounds []json_util.RawMessage
|
||||||
|
@ -28,6 +29,7 @@ type SubJsonService struct {
|
||||||
SubService *SubService
|
SubService *SubService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSubJsonService creates a new JSON subscription service with the given configuration.
|
||||||
func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService {
|
func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService {
|
||||||
var configJson map[string]any
|
var configJson map[string]any
|
||||||
var defaultOutbounds []json_util.RawMessage
|
var defaultOutbounds []json_util.RawMessage
|
||||||
|
@ -67,6 +69,7 @@ func NewSubJsonService(fragment string, noises string, mux string, rules string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetJson generates a JSON subscription configuration for the given subscription ID and host.
|
||||||
func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
|
func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
|
||||||
inbounds, err := s.SubService.getInboundsBySubId(subId)
|
inbounds, err := s.SubService.getInboundsBySubId(subId)
|
||||||
if err != nil || len(inbounds) == 0 {
|
if err != nil || len(inbounds) == 0 {
|
||||||
|
@ -171,12 +174,12 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
||||||
case "tls":
|
case "tls":
|
||||||
if newStream["security"] != "tls" {
|
if newStream["security"] != "tls" {
|
||||||
newStream["security"] = "tls"
|
newStream["security"] = "tls"
|
||||||
newStream["tslSettings"] = map[string]any{}
|
newStream["tlsSettings"] = map[string]any{}
|
||||||
}
|
}
|
||||||
case "none":
|
case "none":
|
||||||
if newStream["security"] != "none" {
|
if newStream["security"] != "none" {
|
||||||
newStream["security"] = "none"
|
newStream["security"] = "none"
|
||||||
delete(newStream, "tslSettings")
|
delete(newStream, "tlsSettings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
streamSettings, _ := json.MarshalIndent(newStream, "", " ")
|
streamSettings, _ := json.MarshalIndent(newStream, "", " ")
|
||||||
|
@ -185,13 +188,9 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
||||||
|
|
||||||
switch inbound.Protocol {
|
switch inbound.Protocol {
|
||||||
case "vmess":
|
case "vmess":
|
||||||
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client, ""))
|
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client))
|
||||||
case "vless":
|
case "vless":
|
||||||
var vlessSettings model.VLESSSettings
|
newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client))
|
||||||
_ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings)
|
|
||||||
|
|
||||||
newOutbounds = append(newOutbounds,
|
|
||||||
s.genVnext(inbound, streamSettings, client, vlessSettings.Encryption))
|
|
||||||
case "trojan", "shadowsocks":
|
case "trojan", "shadowsocks":
|
||||||
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
|
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
|
||||||
}
|
}
|
||||||
|
@ -290,7 +289,35 @@ func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
|
||||||
return rltyData
|
return rltyData
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, encryption string) json_util.RawMessage {
|
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||||
|
outbound := Outbound{}
|
||||||
|
usersData := make([]UserVnext, 1)
|
||||||
|
|
||||||
|
usersData[0].ID = client.ID
|
||||||
|
usersData[0].Email = client.Email
|
||||||
|
usersData[0].Security = client.Security
|
||||||
|
vnextData := make([]VnextSetting, 1)
|
||||||
|
vnextData[0] = VnextSetting{
|
||||||
|
Address: inbound.Listen,
|
||||||
|
Port: inbound.Port,
|
||||||
|
Users: usersData,
|
||||||
|
}
|
||||||
|
|
||||||
|
outbound.Protocol = string(inbound.Protocol)
|
||||||
|
outbound.Tag = "proxy"
|
||||||
|
if s.mux != "" {
|
||||||
|
outbound.Mux = json_util.RawMessage(s.mux)
|
||||||
|
}
|
||||||
|
outbound.StreamSettings = streamSettings
|
||||||
|
outbound.Settings = map[string]any{
|
||||||
|
"vnext": vnextData,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||||
outbound := Outbound{}
|
outbound := Outbound{}
|
||||||
outbound.Protocol = string(inbound.Protocol)
|
outbound.Protocol = string(inbound.Protocol)
|
||||||
outbound.Tag = "proxy"
|
outbound.Tag = "proxy"
|
||||||
|
@ -298,20 +325,22 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
|
||||||
outbound.Mux = json_util.RawMessage(s.mux)
|
outbound.Mux = json_util.RawMessage(s.mux)
|
||||||
}
|
}
|
||||||
outbound.StreamSettings = streamSettings
|
outbound.StreamSettings = streamSettings
|
||||||
// Emit flattened settings inside Settings to match new Xray format
|
|
||||||
settings := make(map[string]any)
|
settings := make(map[string]any)
|
||||||
settings["address"] = inbound.Listen
|
settings["address"] = inbound.Listen
|
||||||
settings["port"] = inbound.Port
|
settings["port"] = inbound.Port
|
||||||
settings["id"] = client.ID
|
settings["id"] = client.ID
|
||||||
if inbound.Protocol == model.VLESS {
|
if client.Flow != "" {
|
||||||
settings["flow"] = client.Flow
|
settings["flow"] = client.Flow
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add encryption for VLESS outbound from inbound settings
|
||||||
|
var inboundSettings map[string]any
|
||||||
|
json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
|
||||||
|
if encryption, ok := inboundSettings["encryption"].(string); ok {
|
||||||
settings["encryption"] = encryption
|
settings["encryption"] = encryption
|
||||||
}
|
}
|
||||||
if inbound.Protocol == model.VMESS {
|
|
||||||
settings["security"] = client.Security
|
|
||||||
}
|
|
||||||
outbound.Settings = settings
|
|
||||||
|
|
||||||
|
outbound.Settings = settings
|
||||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -363,7 +392,17 @@ type Outbound struct {
|
||||||
Settings map[string]any `json:"settings,omitempty"`
|
Settings map[string]any `json:"settings,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy vnext-related structs removed for flattened schema
|
type VnextSetting struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Users []UserVnext `json:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserVnext struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
Security string `json:"security,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type ServerSetting struct {
|
type ServerSetting struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SubService provides business logic for generating subscription links and managing subscription data.
|
||||||
type SubService struct {
|
type SubService struct {
|
||||||
address string
|
address string
|
||||||
showInfo bool
|
showInfo bool
|
||||||
|
@ -29,6 +30,7 @@ type SubService struct {
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSubService creates a new subscription service with the given configuration.
|
||||||
func NewSubService(showInfo bool, remarkModel string) *SubService {
|
func NewSubService(showInfo bool, remarkModel string) *SubService {
|
||||||
return &SubService{
|
return &SubService{
|
||||||
showInfo: showInfo,
|
showInfo: showInfo,
|
||||||
|
@ -36,6 +38,7 @@ func NewSubService(showInfo bool, remarkModel string) *SubService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSubs retrieves subscription links for a given subscription ID and host.
|
||||||
func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.ClientTraffic, error) {
|
func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.ClientTraffic, error) {
|
||||||
s.address = host
|
s.address = host
|
||||||
var result []string
|
var result []string
|
||||||
|
@ -335,9 +338,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string, server *
|
||||||
if inbound.Protocol != model.VLESS {
|
if inbound.Protocol != model.VLESS {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
var vlessSettings model.VLESSSettings
|
|
||||||
_ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings)
|
|
||||||
|
|
||||||
var stream map[string]any
|
var stream map[string]any
|
||||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||||
clients, _ := s.inboundService.GetClients(inbound)
|
clients, _ := s.inboundService.GetClients(inbound)
|
||||||
|
@ -352,11 +352,15 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string, server *
|
||||||
port := inbound.Port
|
port := inbound.Port
|
||||||
streamNetwork := stream["network"].(string)
|
streamNetwork := stream["network"].(string)
|
||||||
params := make(map[string]string)
|
params := make(map[string]string)
|
||||||
if vlessSettings.Encryption != "" {
|
|
||||||
params["encryption"] = vlessSettings.Encryption
|
|
||||||
}
|
|
||||||
params["type"] = streamNetwork
|
params["type"] = streamNetwork
|
||||||
|
|
||||||
|
// Add encryption parameter for VLESS from inbound settings
|
||||||
|
var settings map[string]any
|
||||||
|
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||||
|
if encryption, ok := settings["encryption"].(string); ok {
|
||||||
|
params["encryption"] = encryption
|
||||||
|
}
|
||||||
|
|
||||||
switch streamNetwork {
|
switch streamNetwork {
|
||||||
case "tcp":
|
case "tcp":
|
||||||
tcp, _ := stream["tcpSettings"].(map[string]any)
|
tcp, _ := stream["tcpSettings"].(map[string]any)
|
||||||
|
@ -1029,6 +1033,7 @@ func searchHost(headers any) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageData is a view model for subpage.html
|
// PageData is a view model for subpage.html
|
||||||
|
// PageData contains data for rendering the subscription information page.
|
||||||
type PageData struct {
|
type PageData struct {
|
||||||
Host string
|
Host string
|
||||||
BasePath string
|
BasePath string
|
||||||
|
@ -1050,6 +1055,7 @@ type PageData struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveRequest extracts scheme and host info from request/headers consistently.
|
// ResolveRequest extracts scheme and host info from request/headers consistently.
|
||||||
|
// ResolveRequest extracts scheme, host, and header information from an HTTP request.
|
||||||
func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) {
|
func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) {
|
||||||
// scheme
|
// scheme
|
||||||
scheme = "http"
|
scheme = "http"
|
||||||
|
@ -1092,22 +1098,77 @@ func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildURLs constructs absolute subscription and json URLs.
|
// BuildURLs constructs absolute subscription and JSON subscription URLs for a given subscription ID.
|
||||||
|
// It prioritizes configured URIs, then individual settings, and finally falls back to request-derived components.
|
||||||
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) {
|
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) {
|
||||||
if strings.HasSuffix(subPath, "/") {
|
// Input validation
|
||||||
subURL = scheme + "://" + hostWithPort + subPath + subId
|
if subId == "" {
|
||||||
} else {
|
return "", ""
|
||||||
subURL = scheme + "://" + hostWithPort + strings.TrimRight(subPath, "/") + "/" + subId
|
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(subJsonPath, "/") {
|
|
||||||
subJsonURL = scheme + "://" + hostWithPort + subJsonPath + subId
|
// Get configured URIs first (highest priority)
|
||||||
} else {
|
configuredSubURI, _ := s.settingService.GetSubURI()
|
||||||
subJsonURL = scheme + "://" + hostWithPort + strings.TrimRight(subJsonPath, "/") + "/" + subId
|
configuredSubJsonURI, _ := s.settingService.GetSubJsonURI()
|
||||||
|
|
||||||
|
// Determine base scheme and host (cached to avoid duplicate calls)
|
||||||
|
var baseScheme, baseHostWithPort string
|
||||||
|
if configuredSubURI == "" || configuredSubJsonURI == "" {
|
||||||
|
baseScheme, baseHostWithPort = s.getBaseSchemeAndHost(scheme, hostWithPort)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
|
// Build subscription URL
|
||||||
|
subURL = s.buildSingleURL(configuredSubURI, baseScheme, baseHostWithPort, subPath, subId)
|
||||||
|
|
||||||
|
// Build JSON subscription URL
|
||||||
|
subJsonURL = s.buildSingleURL(configuredSubJsonURI, baseScheme, baseHostWithPort, subJsonPath, subId)
|
||||||
|
|
||||||
|
return subURL, subJsonURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBaseSchemeAndHost determines the base scheme and host from settings or falls back to request values
|
||||||
|
func (s *SubService) getBaseSchemeAndHost(requestScheme, requestHostWithPort string) (string, string) {
|
||||||
|
subDomain, err := s.settingService.GetSubDomain()
|
||||||
|
if err != nil || subDomain == "" {
|
||||||
|
return requestScheme, requestHostWithPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get port and TLS settings
|
||||||
|
subPort, _ := s.settingService.GetSubPort()
|
||||||
|
subKeyFile, _ := s.settingService.GetSubKeyFile()
|
||||||
|
subCertFile, _ := s.settingService.GetSubCertFile()
|
||||||
|
|
||||||
|
// Determine scheme from TLS configuration
|
||||||
|
scheme := "http"
|
||||||
|
if subKeyFile != "" && subCertFile != "" {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build host:port, always include port for clarity
|
||||||
|
hostWithPort := fmt.Sprintf("%s:%d", subDomain, subPort)
|
||||||
|
|
||||||
|
return scheme, hostWithPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSingleURL constructs a single URL using configured URI or base components
|
||||||
|
func (s *SubService) buildSingleURL(configuredURI, baseScheme, baseHostWithPort, basePath, subId string) string {
|
||||||
|
if configuredURI != "" {
|
||||||
|
return s.joinPathWithID(configuredURI, subId)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := fmt.Sprintf("%s://%s", baseScheme, baseHostWithPort)
|
||||||
|
return s.joinPathWithID(baseURL+basePath, subId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// joinPathWithID safely joins a base path with a subscription ID
|
||||||
|
func (s *SubService) joinPathWithID(basePath, subId string) string {
|
||||||
|
if strings.HasSuffix(basePath, "/") {
|
||||||
|
return basePath + subId
|
||||||
|
}
|
||||||
|
return basePath + "/" + subId
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildPageData parses header and prepares the template view model.
|
// BuildPageData parses header and prepares the template view model.
|
||||||
|
// 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) PageData {
|
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string) PageData {
|
||||||
download := common.FormatTraffic(traffic.Down)
|
download := common.FormatTraffic(traffic.Down)
|
||||||
upload := common.FormatTraffic(traffic.Up)
|
upload := common.FormatTraffic(traffic.Up)
|
||||||
|
@ -1116,10 +1177,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
|
||||||
remained := ""
|
remained := ""
|
||||||
if traffic.Total > 0 {
|
if traffic.Total > 0 {
|
||||||
total = common.FormatTraffic(traffic.Total)
|
total = common.FormatTraffic(traffic.Total)
|
||||||
left := traffic.Total - (traffic.Up + traffic.Down)
|
left := max(traffic.Total-(traffic.Up+traffic.Down), 0)
|
||||||
if left < 0 {
|
|
||||||
left = 0
|
|
||||||
}
|
|
||||||
remained = common.FormatTraffic(left)
|
remained = common.FormatTraffic(left)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Package common provides common utility functions for error handling, formatting, and multi-error management.
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -7,16 +8,19 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NewErrorf creates a new error with formatted message.
|
||||||
func NewErrorf(format string, a ...any) error {
|
func NewErrorf(format string, a ...any) error {
|
||||||
msg := fmt.Sprintf(format, a...)
|
msg := fmt.Sprintf(format, a...)
|
||||||
return errors.New(msg)
|
return errors.New(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewError creates a new error from the given arguments.
|
||||||
func NewError(a ...any) error {
|
func NewError(a ...any) error {
|
||||||
msg := fmt.Sprintln(a...)
|
msg := fmt.Sprintln(a...)
|
||||||
return errors.New(msg)
|
return errors.New(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recover handles panic recovery and logs the panic error if a message is provided.
|
||||||
func Recover(msg string) any {
|
func Recover(msg string) any {
|
||||||
panicErr := recover()
|
panicErr := recover()
|
||||||
if panicErr != nil {
|
if panicErr != nil {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FormatTraffic formats traffic bytes into human-readable units (B, KB, MB, GB, TB, PB).
|
||||||
func FormatTraffic(trafficBytes int64) string {
|
func FormatTraffic(trafficBytes int64) string {
|
||||||
units := []string{"B", "KB", "MB", "GB", "TB", "PB"}
|
units := []string{"B", "KB", "MB", "GB", "TB", "PB"}
|
||||||
unitIndex := 0
|
unitIndex := 0
|
||||||
|
|
|
@ -4,8 +4,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// multiError represents a collection of errors.
|
||||||
type multiError []error
|
type multiError []error
|
||||||
|
|
||||||
|
// Error returns a string representation of all errors joined with " | ".
|
||||||
func (e multiError) Error() string {
|
func (e multiError) Error() string {
|
||||||
var r strings.Builder
|
var r strings.Builder
|
||||||
r.WriteString("multierr: ")
|
r.WriteString("multierr: ")
|
||||||
|
@ -16,6 +18,7 @@ func (e multiError) Error() string {
|
||||||
return r.String()
|
return r.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Combine combines multiple errors into a single error, filtering out nil errors.
|
||||||
func Combine(maybeError ...error) error {
|
func Combine(maybeError ...error) error {
|
||||||
var errs multiError
|
var errs multiError
|
||||||
for _, err := range maybeError {
|
for _, err := range maybeError {
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
|
// Package crypto provides cryptographic utilities for password hashing and verification.
|
||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// HashPasswordAsBcrypt generates a bcrypt hash of the given password.
|
||||||
func HashPasswordAsBcrypt(password string) (string, error) {
|
func HashPasswordAsBcrypt(password string) (string, error) {
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
return string(hash), err
|
return string(hash), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckPasswordHash verifies if the given password matches the bcrypt hash.
|
||||||
func CheckPasswordHash(hash, password string) bool {
|
func CheckPasswordHash(hash, password string) bool {
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
return err == nil
|
return err == nil
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
|
// Package json_util provides JSON utilities including a custom RawMessage type.
|
||||||
package json_util
|
package json_util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RawMessage is a custom JSON raw message type that marshals empty slices as "null".
|
||||||
type RawMessage []byte
|
type RawMessage []byte
|
||||||
|
|
||||||
// MarshalJSON: Customize json.RawMessage default behavior
|
// MarshalJSON customizes the JSON marshaling behavior for RawMessage.
|
||||||
|
// Empty RawMessage values are marshaled as "null" instead of "[]".
|
||||||
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||||
if len(m) == 0 {
|
if len(m) == 0 {
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
|
@ -14,7 +17,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON: sets *m to a copy of data.
|
// UnmarshalJSON sets *m to a copy of the JSON data.
|
||||||
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
|
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
// Package random provides utilities for generating random strings and numbers.
|
||||||
package random
|
package random
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
"crypto/rand"
|
||||||
|
"math/big"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -13,6 +15,8 @@ var (
|
||||||
allSeq [62]rune
|
allSeq [62]rune
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// init initializes the character sequences used for random string generation.
|
||||||
|
// It sets up arrays for numbers, lowercase letters, uppercase letters, and combinations.
|
||||||
func init() {
|
func init() {
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
numSeq[i] = rune('0' + i)
|
numSeq[i] = rune('0' + i)
|
||||||
|
@ -33,14 +37,25 @@ func init() {
|
||||||
copy(allSeq[len(numSeq)+len(lowerSeq):], upperSeq[:])
|
copy(allSeq[len(numSeq)+len(lowerSeq):], upperSeq[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seq generates a random string of length n containing alphanumeric characters (numbers, lowercase and uppercase letters).
|
||||||
func Seq(n int) string {
|
func Seq(n int) string {
|
||||||
runes := make([]rune, n)
|
runes := make([]rune, n)
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
runes[i] = allSeq[rand.Intn(len(allSeq))]
|
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(allSeq))))
|
||||||
|
if err != nil {
|
||||||
|
panic("crypto/rand failed: " + err.Error())
|
||||||
|
}
|
||||||
|
runes[i] = allSeq[idx.Int64()]
|
||||||
}
|
}
|
||||||
return string(runes)
|
return string(runes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Num generates a random integer between 0 and n-1.
|
||||||
func Num(n int) int {
|
func Num(n int) int {
|
||||||
return rand.Intn(n)
|
bn := big.NewInt(int64(n))
|
||||||
|
r, err := rand.Int(rand.Reader, bn)
|
||||||
|
if err != nil {
|
||||||
|
panic("crypto/rand failed: " + err.Error())
|
||||||
|
}
|
||||||
|
return int(r.Int64())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
// Package reflect_util provides reflection utilities for working with struct fields and values.
|
||||||
package reflect_util
|
package reflect_util
|
||||||
|
|
||||||
import "reflect"
|
import "reflect"
|
||||||
|
|
||||||
|
// GetFields returns all struct fields of the given reflect.Type.
|
||||||
func GetFields(t reflect.Type) []reflect.StructField {
|
func GetFields(t reflect.Type) []reflect.StructField {
|
||||||
num := t.NumField()
|
num := t.NumField()
|
||||||
fields := make([]reflect.StructField, 0, num)
|
fields := make([]reflect.StructField, 0, num)
|
||||||
|
@ -11,6 +13,7 @@ func GetFields(t reflect.Type) []reflect.StructField {
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFieldValues returns all field values of the given reflect.Value.
|
||||||
func GetFieldValues(v reflect.Value) []reflect.Value {
|
func GetFieldValues(v reflect.Value) []reflect.Value {
|
||||||
num := v.NumField()
|
num := v.NumField()
|
||||||
fields := make([]reflect.Value, 0, num)
|
fields := make([]reflect.Value, 0, num)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package sys provides system utilities for monitoring network connections and CPU usage.
|
||||||
|
// Platform-specific implementations are provided for Windows, Linux, and macOS.
|
||||||
package sys
|
package sys
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -45,6 +45,8 @@ func getLinesNum(filename string) (int, error) {
|
||||||
return sum, nil
|
return sum, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTCPCount returns the number of active TCP connections by reading
|
||||||
|
// /proc/net/tcp and /proc/net/tcp6 when available.
|
||||||
func GetTCPCount() (int, error) {
|
func GetTCPCount() (int, error) {
|
||||||
root := HostProc()
|
root := HostProc()
|
||||||
|
|
||||||
|
@ -75,6 +77,8 @@ func GetUDPCount() (int, error) {
|
||||||
return udp4 + udp6, nil
|
return udp4 + udp6, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// safeGetLinesNum returns 0 if the file does not exist, otherwise forwards
|
||||||
|
// to getLinesNum to count the number of lines.
|
||||||
func safeGetLinesNum(path string) (int, error) {
|
func safeGetLinesNum(path string) (int, error) {
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/shirou/gopsutil/v4/net"
|
"github.com/shirou/gopsutil/v4/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetConnectionCount returns the number of active connections for the specified protocol ("tcp" or "udp").
|
||||||
func GetConnectionCount(proto string) (int, error) {
|
func GetConnectionCount(proto string) (int, error) {
|
||||||
if proto != "tcp" && proto != "udp" {
|
if proto != "tcp" && proto != "udp" {
|
||||||
return 0, errors.New("invalid protocol")
|
return 0, errors.New("invalid protocol")
|
||||||
|
@ -24,10 +25,12 @@ func GetConnectionCount(proto string) (int, error) {
|
||||||
return len(stats), nil
|
return len(stats), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTCPCount returns the number of active TCP connections.
|
||||||
func GetTCPCount() (int, error) {
|
func GetTCPCount() (int, error) {
|
||||||
return GetConnectionCount("tcp")
|
return GetConnectionCount("tcp")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUDPCount returns the number of active UDP connections.
|
||||||
func GetUDPCount() (int, error) {
|
func GetUDPCount() (int, error) {
|
||||||
return GetConnectionCount("udp")
|
return GetConnectionCount("udp")
|
||||||
}
|
}
|
||||||
|
@ -50,6 +53,8 @@ type filetime struct {
|
||||||
HighDateTime uint32
|
HighDateTime uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ftToUint64 converts a Windows FILETIME-like struct to a uint64 for
|
||||||
|
// arithmetic and delta calculations used by CPUPercentRaw.
|
||||||
func ftToUint64(ft filetime) uint64 {
|
func ftToUint64(ft filetime) uint64 {
|
||||||
return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime)
|
return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime)
|
||||||
}
|
}
|
||||||
|
|
2
web/assets/css/custom.min.css
vendored
2
web/assets/css/custom.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -686,14 +686,7 @@ class Outbound extends CommonClass {
|
||||||
if (this.stream?.sockopt)
|
if (this.stream?.sockopt)
|
||||||
stream = { sockopt: this.stream.sockopt.toJson() };
|
stream = { sockopt: this.stream.sockopt.toJson() };
|
||||||
}
|
}
|
||||||
// For VMess/VLESS, emit settings as a flat object
|
|
||||||
let settingsOut = this.settings instanceof CommonClass ? this.settings.toJson() : this.settings;
|
let settingsOut = this.settings instanceof CommonClass ? this.settings.toJson() : this.settings;
|
||||||
// Remove undefined/null keys
|
|
||||||
if (settingsOut && typeof settingsOut === 'object') {
|
|
||||||
Object.keys(settingsOut).forEach(k => {
|
|
||||||
if (settingsOut[k] === undefined || settingsOut[k] === null) delete settingsOut[k];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
protocol: this.protocol,
|
protocol: this.protocol,
|
||||||
settings: settingsOut,
|
settings: settingsOut,
|
||||||
|
@ -1031,21 +1024,28 @@ Outbound.VmessSettings = class extends CommonClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json = {}) {
|
static fromJson(json = {}) {
|
||||||
if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VmessSettings();
|
if (!ObjectUtil.isArrEmpty(json.vnext)) {
|
||||||
|
const v = json.vnext[0] || {};
|
||||||
|
const u = ObjectUtil.isArrEmpty(v.users) ? {} : v.users[0];
|
||||||
return new Outbound.VmessSettings(
|
return new Outbound.VmessSettings(
|
||||||
json.address,
|
v.address,
|
||||||
json.port,
|
v.port,
|
||||||
json.id,
|
u.id,
|
||||||
json.security,
|
u.security,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
toJson() {
|
toJson() {
|
||||||
return {
|
return {
|
||||||
|
vnext: [{
|
||||||
address: this.address,
|
address: this.address,
|
||||||
port: this.port,
|
port: this.port,
|
||||||
|
users: [{
|
||||||
id: this.id,
|
id: this.id,
|
||||||
security: this.security,
|
security: this.security
|
||||||
|
}]
|
||||||
|
}]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@ class AllSetting {
|
||||||
this.webKeyFile = "";
|
this.webKeyFile = "";
|
||||||
this.webBasePath = "/";
|
this.webBasePath = "/";
|
||||||
this.sessionMaxAge = 360;
|
this.sessionMaxAge = 360;
|
||||||
this.pageSize = 50;
|
this.pageSize = 25;
|
||||||
this.expireDiff = 0;
|
this.expireDiff = 0;
|
||||||
this.trafficDiff = 0;
|
this.trafficDiff = 0;
|
||||||
this.remarkModel = "-ieo";
|
this.remarkModel = "-ieo";
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// APIController handles the main API routes for the 3x-ui panel, including inbounds and server management.
|
||||||
type APIController struct {
|
type APIController struct {
|
||||||
BaseController
|
BaseController
|
||||||
inboundController *InboundController
|
inboundController *InboundController
|
||||||
|
@ -13,12 +14,14 @@ type APIController struct {
|
||||||
Tgbot service.Tgbot
|
Tgbot service.Tgbot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewAPIController creates a new APIController instance and initializes its routes.
|
||||||
func NewAPIController(g *gin.RouterGroup) *APIController {
|
func NewAPIController(g *gin.RouterGroup) *APIController {
|
||||||
a := &APIController{}
|
a := &APIController{}
|
||||||
a.initRouter(g)
|
a.initRouter(g)
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initRouter sets up the API routes for inbounds, server, and other endpoints.
|
||||||
func (a *APIController) initRouter(g *gin.RouterGroup) {
|
func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||||
// Main API group
|
// Main API group
|
||||||
api := g.Group("/panel/api")
|
api := g.Group("/panel/api")
|
||||||
|
@ -36,6 +39,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||||
api.GET("/backuptotgbot", a.BackuptoTgbot)
|
api.GET("/backuptotgbot", a.BackuptoTgbot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BackuptoTgbot sends a backup of the panel data to Telegram bot admins.
|
||||||
func (a *APIController) BackuptoTgbot(c *gin.Context) {
|
func (a *APIController) BackuptoTgbot(c *gin.Context) {
|
||||||
a.Tgbot.SendBackupToAdmins()
|
a.Tgbot.SendBackupToAdmins()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package controller provides HTTP request handlers and controllers for the 3x-ui web management panel.
|
||||||
|
// It handles routing, authentication, and API endpoints for managing Xray inbounds, settings, and more.
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -10,8 +12,10 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BaseController provides common functionality for all controllers, including authentication checks.
|
||||||
type BaseController struct{}
|
type BaseController struct{}
|
||||||
|
|
||||||
|
// checkLogin is a middleware that verifies user authentication and handles unauthorized access.
|
||||||
func (a *BaseController) checkLogin(c *gin.Context) {
|
func (a *BaseController) checkLogin(c *gin.Context) {
|
||||||
if !session.IsLogin(c) {
|
if !session.IsLogin(c) {
|
||||||
if isAjax(c) {
|
if isAjax(c) {
|
||||||
|
@ -25,6 +29,7 @@ func (a *BaseController) checkLogin(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// I18nWeb retrieves an internationalized message for the web interface based on the current locale.
|
||||||
func I18nWeb(c *gin.Context, name string, params ...string) string {
|
func I18nWeb(c *gin.Context, name string, params ...string) string {
|
||||||
anyfunc, funcExists := c.Get("I18n")
|
anyfunc, funcExists := c.Get("I18n")
|
||||||
if !funcExists {
|
if !funcExists {
|
||||||
|
|
|
@ -13,17 +13,20 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// InboundController handles HTTP requests related to Xray inbounds management.
|
||||||
type InboundController struct {
|
type InboundController struct {
|
||||||
inboundService service.InboundService
|
inboundService service.InboundService
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewInboundController creates a new InboundController and sets up its routes.
|
||||||
func NewInboundController(g *gin.RouterGroup) *InboundController {
|
func NewInboundController(g *gin.RouterGroup) *InboundController {
|
||||||
a := &InboundController{}
|
a := &InboundController{}
|
||||||
a.initRouter(g)
|
a.initRouter(g)
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initRouter initializes the routes for inbound-related operations.
|
||||||
func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||||
|
|
||||||
g.GET("/list", a.getInbounds)
|
g.GET("/list", a.getInbounds)
|
||||||
|
@ -50,6 +53,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||||
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
|
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getInbounds retrieves the list of inbounds for the logged-in user.
|
||||||
func (a *InboundController) getInbounds(c *gin.Context) {
|
func (a *InboundController) getInbounds(c *gin.Context) {
|
||||||
user := session.GetLoginUser(c)
|
user := session.GetLoginUser(c)
|
||||||
inbounds, err := a.inboundService.GetInbounds(user.Id)
|
inbounds, err := a.inboundService.GetInbounds(user.Id)
|
||||||
|
@ -60,6 +64,7 @@ func (a *InboundController) getInbounds(c *gin.Context) {
|
||||||
jsonObj(c, inbounds, nil)
|
jsonObj(c, inbounds, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getInbound retrieves a specific inbound by its ID.
|
||||||
func (a *InboundController) getInbound(c *gin.Context) {
|
func (a *InboundController) getInbound(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -74,6 +79,7 @@ func (a *InboundController) getInbound(c *gin.Context) {
|
||||||
jsonObj(c, inbound, nil)
|
jsonObj(c, inbound, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getClientTraffics retrieves client traffic information by email.
|
||||||
func (a *InboundController) getClientTraffics(c *gin.Context) {
|
func (a *InboundController) getClientTraffics(c *gin.Context) {
|
||||||
email := c.Param("email")
|
email := c.Param("email")
|
||||||
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
|
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
|
||||||
|
@ -84,6 +90,7 @@ func (a *InboundController) getClientTraffics(c *gin.Context) {
|
||||||
jsonObj(c, clientTraffics, nil)
|
jsonObj(c, clientTraffics, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getClientTrafficsById retrieves client traffic information by inbound ID.
|
||||||
func (a *InboundController) getClientTrafficsById(c *gin.Context) {
|
func (a *InboundController) getClientTrafficsById(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
clientTraffics, err := a.inboundService.GetClientTrafficByID(id)
|
clientTraffics, err := a.inboundService.GetClientTrafficByID(id)
|
||||||
|
@ -94,6 +101,7 @@ func (a *InboundController) getClientTrafficsById(c *gin.Context) {
|
||||||
jsonObj(c, clientTraffics, nil)
|
jsonObj(c, clientTraffics, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addInbound creates a new inbound configuration.
|
||||||
func (a *InboundController) addInbound(c *gin.Context) {
|
func (a *InboundController) addInbound(c *gin.Context) {
|
||||||
inbound := &model.Inbound{}
|
inbound := &model.Inbound{}
|
||||||
err := c.ShouldBind(inbound)
|
err := c.ShouldBind(inbound)
|
||||||
|
@ -120,6 +128,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delInbound deletes an inbound configuration by its ID.
|
||||||
func (a *InboundController) delInbound(c *gin.Context) {
|
func (a *InboundController) delInbound(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -137,6 +146,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateInbound updates an existing inbound configuration.
|
||||||
func (a *InboundController) updateInbound(c *gin.Context) {
|
func (a *InboundController) updateInbound(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -162,6 +172,7 @@ func (a *InboundController) updateInbound(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getClientIps retrieves the IP addresses associated with a client by email.
|
||||||
func (a *InboundController) getClientIps(c *gin.Context) {
|
func (a *InboundController) getClientIps(c *gin.Context) {
|
||||||
email := c.Param("email")
|
email := c.Param("email")
|
||||||
|
|
||||||
|
@ -174,6 +185,7 @@ func (a *InboundController) getClientIps(c *gin.Context) {
|
||||||
jsonObj(c, ips, nil)
|
jsonObj(c, ips, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clearClientIps clears the IP addresses for a client by email.
|
||||||
func (a *InboundController) clearClientIps(c *gin.Context) {
|
func (a *InboundController) clearClientIps(c *gin.Context) {
|
||||||
email := c.Param("email")
|
email := c.Param("email")
|
||||||
|
|
||||||
|
@ -185,6 +197,7 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.logCleanSuccess"), nil)
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.logCleanSuccess"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addInboundClient adds a new client to an existing inbound.
|
||||||
func (a *InboundController) addInboundClient(c *gin.Context) {
|
func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||||
data := &model.Inbound{}
|
data := &model.Inbound{}
|
||||||
err := c.ShouldBind(data)
|
err := c.ShouldBind(data)
|
||||||
|
@ -204,6 +217,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delInboundClient deletes a client from an inbound by inbound ID and client ID.
|
||||||
func (a *InboundController) delInboundClient(c *gin.Context) {
|
func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -223,6 +237,7 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateInboundClient updates a client's configuration in an inbound.
|
||||||
func (a *InboundController) updateInboundClient(c *gin.Context) {
|
func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||||
clientId := c.Param("clientId")
|
clientId := c.Param("clientId")
|
||||||
|
|
||||||
|
@ -244,6 +259,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resetClientTraffic resets the traffic counter for a specific client in an inbound.
|
||||||
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -263,6 +279,7 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resetAllTraffics resets all traffic counters across all inbounds.
|
||||||
func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
||||||
err := a.inboundService.ResetAllTraffics()
|
err := a.inboundService.ResetAllTraffics()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -274,6 +291,7 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllTrafficSuccess"), nil)
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllTrafficSuccess"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resetAllClientTraffics resets traffic counters for all clients in a specific inbound.
|
||||||
func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -291,6 +309,7 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil)
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// importInbound imports an inbound configuration from provided data.
|
||||||
func (a *InboundController) importInbound(c *gin.Context) {
|
func (a *InboundController) importInbound(c *gin.Context) {
|
||||||
inbound := &model.Inbound{}
|
inbound := &model.Inbound{}
|
||||||
err := json.Unmarshal([]byte(c.PostForm("data")), inbound)
|
err := json.Unmarshal([]byte(c.PostForm("data")), inbound)
|
||||||
|
@ -320,6 +339,7 @@ func (a *InboundController) importInbound(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delDepletedClients deletes clients in an inbound who have exhausted their traffic limits.
|
||||||
func (a *InboundController) delDepletedClients(c *gin.Context) {
|
func (a *InboundController) delDepletedClients(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -334,15 +354,18 @@ func (a *InboundController) delDepletedClients(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.delDepletedClientsSuccess"), nil)
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.delDepletedClientsSuccess"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// onlines retrieves the list of currently online clients.
|
||||||
func (a *InboundController) onlines(c *gin.Context) {
|
func (a *InboundController) onlines(c *gin.Context) {
|
||||||
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lastOnline retrieves the last online timestamps for clients.
|
||||||
func (a *InboundController) lastOnline(c *gin.Context) {
|
func (a *InboundController) lastOnline(c *gin.Context) {
|
||||||
data, err := a.inboundService.GetClientsLastOnline()
|
data, err := a.inboundService.GetClientsLastOnline()
|
||||||
jsonObj(c, data, err)
|
jsonObj(c, data, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateClientTraffic updates the traffic statistics for a client by email.
|
||||||
func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
||||||
email := c.Param("email")
|
email := c.Param("email")
|
||||||
|
|
||||||
|
@ -368,6 +391,7 @@ func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delInboundClientByEmail deletes a client from an inbound by email address.
|
||||||
func (a *InboundController) delInboundClientByEmail(c *gin.Context) {
|
func (a *InboundController) delInboundClientByEmail(c *gin.Context) {
|
||||||
inboundId, err := strconv.Atoi(c.Param("id"))
|
inboundId, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -13,12 +13,14 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LoginForm represents the login request structure.
|
||||||
type LoginForm struct {
|
type LoginForm struct {
|
||||||
Username string `json:"username" form:"username"`
|
Username string `json:"username" form:"username"`
|
||||||
Password string `json:"password" form:"password"`
|
Password string `json:"password" form:"password"`
|
||||||
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
|
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IndexController handles the main index and login-related routes.
|
||||||
type IndexController struct {
|
type IndexController struct {
|
||||||
BaseController
|
BaseController
|
||||||
|
|
||||||
|
@ -27,12 +29,14 @@ type IndexController struct {
|
||||||
tgbot service.Tgbot
|
tgbot service.Tgbot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewIndexController creates a new IndexController and initializes its routes.
|
||||||
func NewIndexController(g *gin.RouterGroup) *IndexController {
|
func NewIndexController(g *gin.RouterGroup) *IndexController {
|
||||||
a := &IndexController{}
|
a := &IndexController{}
|
||||||
a.initRouter(g)
|
a.initRouter(g)
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initRouter sets up the routes for index, login, logout, and two-factor authentication.
|
||||||
func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
||||||
g.GET("/", a.index)
|
g.GET("/", a.index)
|
||||||
g.POST("/login", a.login)
|
g.POST("/login", a.login)
|
||||||
|
@ -40,6 +44,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
||||||
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
|
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// index handles the root route, redirecting logged-in users to the panel or showing the login page.
|
||||||
func (a *IndexController) index(c *gin.Context) {
|
func (a *IndexController) index(c *gin.Context) {
|
||||||
if session.IsLogin(c) {
|
if session.IsLogin(c) {
|
||||||
c.Redirect(http.StatusTemporaryRedirect, "panel/")
|
c.Redirect(http.StatusTemporaryRedirect, "panel/")
|
||||||
|
@ -48,6 +53,7 @@ func (a *IndexController) index(c *gin.Context) {
|
||||||
html(c, "login.html", "pages.login.title", nil)
|
html(c, "login.html", "pages.login.title", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// login handles user authentication and session creation.
|
||||||
func (a *IndexController) login(c *gin.Context) {
|
func (a *IndexController) login(c *gin.Context) {
|
||||||
var form LoginForm
|
var form LoginForm
|
||||||
|
|
||||||
|
@ -95,6 +101,7 @@ func (a *IndexController) login(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil)
|
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logout handles user logout by clearing the session and redirecting to the login page.
|
||||||
func (a *IndexController) logout(c *gin.Context) {
|
func (a *IndexController) logout(c *gin.Context) {
|
||||||
user := session.GetLoginUser(c)
|
user := session.GetLoginUser(c)
|
||||||
if user != nil {
|
if user != nil {
|
||||||
|
@ -107,6 +114,7 @@ func (a *IndexController) logout(c *gin.Context) {
|
||||||
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getTwoFactorEnable retrieves the current status of two-factor authentication.
|
||||||
func (a *IndexController) getTwoFactorEnable(c *gin.Context) {
|
func (a *IndexController) getTwoFactorEnable(c *gin.Context) {
|
||||||
status, err := a.settingService.GetTwoFactorEnable()
|
status, err := a.settingService.GetTwoFactorEnable()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
|
|
||||||
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
|
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
|
||||||
|
|
||||||
|
// ServerController handles server management and status-related operations.
|
||||||
type ServerController struct {
|
type ServerController struct {
|
||||||
BaseController
|
BaseController
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ type ServerController struct {
|
||||||
lastGetVersionsTime int64 // unix seconds
|
lastGetVersionsTime int64 // unix seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewServerController creates a new ServerController, initializes routes, and starts background tasks.
|
||||||
func NewServerController(g *gin.RouterGroup) *ServerController {
|
func NewServerController(g *gin.RouterGroup) *ServerController {
|
||||||
a := &ServerController{}
|
a := &ServerController{}
|
||||||
a.initRouter(g)
|
a.initRouter(g)
|
||||||
|
@ -34,6 +36,7 @@ func NewServerController(g *gin.RouterGroup) *ServerController {
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initRouter sets up the routes for server status, Xray management, and utility endpoints.
|
||||||
func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||||
|
|
||||||
g.GET("/status", a.status)
|
g.GET("/status", a.status)
|
||||||
|
@ -58,6 +61,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||||
g.POST("/getNewEchCert", a.getNewEchCert)
|
g.POST("/getNewEchCert", a.getNewEchCert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// refreshStatus updates the cached server status and collects CPU history.
|
||||||
func (a *ServerController) refreshStatus() {
|
func (a *ServerController) refreshStatus() {
|
||||||
a.lastStatus = a.serverService.GetStatus(a.lastStatus)
|
a.lastStatus = a.serverService.GetStatus(a.lastStatus)
|
||||||
// collect cpu history when status is fresh
|
// collect cpu history when status is fresh
|
||||||
|
@ -66,6 +70,7 @@ func (a *ServerController) refreshStatus() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// startTask initiates background tasks for continuous status monitoring.
|
||||||
func (a *ServerController) startTask() {
|
func (a *ServerController) startTask() {
|
||||||
webServer := global.GetWebServer()
|
webServer := global.GetWebServer()
|
||||||
c := webServer.GetCron()
|
c := webServer.GetCron()
|
||||||
|
@ -76,8 +81,10 @@ func (a *ServerController) startTask() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// status returns the current server status information.
|
||||||
func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) }
|
func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) }
|
||||||
|
|
||||||
|
// getCpuHistoryBucket retrieves aggregated CPU usage history based on the specified time bucket.
|
||||||
func (a *ServerController) getCpuHistoryBucket(c *gin.Context) {
|
func (a *ServerController) getCpuHistoryBucket(c *gin.Context) {
|
||||||
bucketStr := c.Param("bucket")
|
bucketStr := c.Param("bucket")
|
||||||
bucket, err := strconv.Atoi(bucketStr)
|
bucket, err := strconv.Atoi(bucketStr)
|
||||||
|
@ -101,6 +108,7 @@ func (a *ServerController) getCpuHistoryBucket(c *gin.Context) {
|
||||||
jsonObj(c, points, nil)
|
jsonObj(c, points, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getXrayVersion retrieves available Xray versions, with caching for 1 minute.
|
||||||
func (a *ServerController) getXrayVersion(c *gin.Context) {
|
func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
if now-a.lastGetVersionsTime <= 60 { // 1 minute cache
|
if now-a.lastGetVersionsTime <= 60 { // 1 minute cache
|
||||||
|
@ -120,18 +128,29 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||||
jsonObj(c, versions, nil)
|
jsonObj(c, versions, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// installXray installs or updates Xray to the specified version.
|
||||||
func (a *ServerController) installXray(c *gin.Context) {
|
func (a *ServerController) installXray(c *gin.Context) {
|
||||||
version := c.Param("version")
|
version := c.Param("version")
|
||||||
err := a.serverService.UpdateXray(version)
|
err := a.serverService.UpdateXray(version)
|
||||||
jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err)
|
jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateGeofile updates the specified geo file for Xray.
|
||||||
func (a *ServerController) updateGeofile(c *gin.Context) {
|
func (a *ServerController) updateGeofile(c *gin.Context) {
|
||||||
fileName := c.Param("fileName")
|
fileName := c.Param("fileName")
|
||||||
|
|
||||||
|
// Validate the filename for security (prevent path traversal attacks)
|
||||||
|
if fileName != "" && !a.serverService.IsValidGeofileName(fileName) {
|
||||||
|
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"),
|
||||||
|
fmt.Errorf("invalid filename: contains unsafe characters or path traversal patterns"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err := a.serverService.UpdateGeofile(fileName)
|
err := a.serverService.UpdateGeofile(fileName)
|
||||||
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err)
|
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stopXrayService stops the Xray service.
|
||||||
func (a *ServerController) stopXrayService(c *gin.Context) {
|
func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||||
err := a.serverService.StopXrayService()
|
err := a.serverService.StopXrayService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -141,6 +160,7 @@ func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.xray.stopSuccess"), err)
|
jsonMsg(c, I18nWeb(c, "pages.xray.stopSuccess"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// restartXrayService restarts the Xray service.
|
||||||
func (a *ServerController) restartXrayService(c *gin.Context) {
|
func (a *ServerController) restartXrayService(c *gin.Context) {
|
||||||
err := a.serverService.RestartXrayService()
|
err := a.serverService.RestartXrayService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -150,6 +170,7 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.xray.restartSuccess"), err)
|
jsonMsg(c, I18nWeb(c, "pages.xray.restartSuccess"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getLogs retrieves the application logs based on count, level, and syslog filters.
|
||||||
func (a *ServerController) getLogs(c *gin.Context) {
|
func (a *ServerController) getLogs(c *gin.Context) {
|
||||||
count := c.Param("count")
|
count := c.Param("count")
|
||||||
level := c.PostForm("level")
|
level := c.PostForm("level")
|
||||||
|
@ -158,6 +179,7 @@ func (a *ServerController) getLogs(c *gin.Context) {
|
||||||
jsonObj(c, logs, nil)
|
jsonObj(c, logs, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getXrayLogs retrieves Xray logs with filtering options for direct, blocked, and proxy traffic.
|
||||||
func (a *ServerController) getXrayLogs(c *gin.Context) {
|
func (a *ServerController) getXrayLogs(c *gin.Context) {
|
||||||
count := c.Param("count")
|
count := c.Param("count")
|
||||||
filter := c.PostForm("filter")
|
filter := c.PostForm("filter")
|
||||||
|
@ -202,6 +224,7 @@ func (a *ServerController) getXrayLogs(c *gin.Context) {
|
||||||
jsonObj(c, logs, nil)
|
jsonObj(c, logs, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getConfigJson retrieves the Xray configuration as JSON.
|
||||||
func (a *ServerController) getConfigJson(c *gin.Context) {
|
func (a *ServerController) getConfigJson(c *gin.Context) {
|
||||||
configJson, err := a.serverService.GetConfigJson()
|
configJson, err := a.serverService.GetConfigJson()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -211,6 +234,7 @@ func (a *ServerController) getConfigJson(c *gin.Context) {
|
||||||
jsonObj(c, configJson, nil)
|
jsonObj(c, configJson, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDb downloads the database file.
|
||||||
func (a *ServerController) getDb(c *gin.Context) {
|
func (a *ServerController) getDb(c *gin.Context) {
|
||||||
db, err := a.serverService.GetDb()
|
db, err := a.serverService.GetDb()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -238,6 +262,7 @@ func isValidFilename(filename string) bool {
|
||||||
return filenameRegex.MatchString(filename)
|
return filenameRegex.MatchString(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// importDB imports a database file and restarts the Xray service.
|
||||||
func (a *ServerController) importDB(c *gin.Context) {
|
func (a *ServerController) importDB(c *gin.Context) {
|
||||||
// Get the file from the request body
|
// Get the file from the request body
|
||||||
file, _, err := c.Request.FormFile("db")
|
file, _, err := c.Request.FormFile("db")
|
||||||
|
@ -258,6 +283,7 @@ func (a *ServerController) importDB(c *gin.Context) {
|
||||||
jsonObj(c, I18nWeb(c, "pages.index.importDatabaseSuccess"), nil)
|
jsonObj(c, I18nWeb(c, "pages.index.importDatabaseSuccess"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getNewX25519Cert generates a new X25519 certificate.
|
||||||
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
|
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
|
||||||
cert, err := a.serverService.GetNewX25519Cert()
|
cert, err := a.serverService.GetNewX25519Cert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -267,6 +293,7 @@ func (a *ServerController) getNewX25519Cert(c *gin.Context) {
|
||||||
jsonObj(c, cert, nil)
|
jsonObj(c, cert, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getNewmldsa65 generates a new ML-DSA-65 key.
|
||||||
func (a *ServerController) getNewmldsa65(c *gin.Context) {
|
func (a *ServerController) getNewmldsa65(c *gin.Context) {
|
||||||
cert, err := a.serverService.GetNewmldsa65()
|
cert, err := a.serverService.GetNewmldsa65()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -276,6 +303,7 @@ func (a *ServerController) getNewmldsa65(c *gin.Context) {
|
||||||
jsonObj(c, cert, nil)
|
jsonObj(c, cert, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getNewEchCert generates a new ECH certificate for the given SNI.
|
||||||
func (a *ServerController) getNewEchCert(c *gin.Context) {
|
func (a *ServerController) getNewEchCert(c *gin.Context) {
|
||||||
sni := c.PostForm("sni")
|
sni := c.PostForm("sni")
|
||||||
cert, err := a.serverService.GetNewEchCert(sni)
|
cert, err := a.serverService.GetNewEchCert(sni)
|
||||||
|
@ -286,6 +314,7 @@ func (a *ServerController) getNewEchCert(c *gin.Context) {
|
||||||
jsonObj(c, cert, nil)
|
jsonObj(c, cert, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getNewVlessEnc generates a new VLESS encryption key.
|
||||||
func (a *ServerController) getNewVlessEnc(c *gin.Context) {
|
func (a *ServerController) getNewVlessEnc(c *gin.Context) {
|
||||||
out, err := a.serverService.GetNewVlessEnc()
|
out, err := a.serverService.GetNewVlessEnc()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -295,6 +324,7 @@ func (a *ServerController) getNewVlessEnc(c *gin.Context) {
|
||||||
jsonObj(c, out, nil)
|
jsonObj(c, out, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getNewUUID generates a new UUID.
|
||||||
func (a *ServerController) getNewUUID(c *gin.Context) {
|
func (a *ServerController) getNewUUID(c *gin.Context) {
|
||||||
uuidResp, err := a.serverService.GetNewUUID()
|
uuidResp, err := a.serverService.GetNewUUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -305,6 +335,7 @@ func (a *ServerController) getNewUUID(c *gin.Context) {
|
||||||
jsonObj(c, uuidResp, nil)
|
jsonObj(c, uuidResp, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getNewmlkem768 generates a new ML-KEM-768 key.
|
||||||
func (a *ServerController) getNewmlkem768(c *gin.Context) {
|
func (a *ServerController) getNewmlkem768(c *gin.Context) {
|
||||||
out, err := a.serverService.GetNewmlkem768()
|
out, err := a.serverService.GetNewmlkem768()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// updateUserForm represents the form for updating user credentials.
|
||||||
type updateUserForm struct {
|
type updateUserForm struct {
|
||||||
OldUsername string `json:"oldUsername" form:"oldUsername"`
|
OldUsername string `json:"oldUsername" form:"oldUsername"`
|
||||||
OldPassword string `json:"oldPassword" form:"oldPassword"`
|
OldPassword string `json:"oldPassword" form:"oldPassword"`
|
||||||
|
@ -19,18 +20,21 @@ type updateUserForm struct {
|
||||||
NewPassword string `json:"newPassword" form:"newPassword"`
|
NewPassword string `json:"newPassword" form:"newPassword"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SettingController handles settings and user management operations.
|
||||||
type SettingController struct {
|
type SettingController struct {
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
userService service.UserService
|
userService service.UserService
|
||||||
panelService service.PanelService
|
panelService service.PanelService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSettingController creates a new SettingController and initializes its routes.
|
||||||
func NewSettingController(g *gin.RouterGroup) *SettingController {
|
func NewSettingController(g *gin.RouterGroup) *SettingController {
|
||||||
a := &SettingController{}
|
a := &SettingController{}
|
||||||
a.initRouter(g)
|
a.initRouter(g)
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initRouter sets up the routes for settings management.
|
||||||
func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
||||||
g = g.Group("/setting")
|
g = g.Group("/setting")
|
||||||
|
|
||||||
|
@ -42,6 +46,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
||||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getAllSetting retrieves all current settings.
|
||||||
func (a *SettingController) getAllSetting(c *gin.Context) {
|
func (a *SettingController) getAllSetting(c *gin.Context) {
|
||||||
allSetting, err := a.settingService.GetAllSetting()
|
allSetting, err := a.settingService.GetAllSetting()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -51,6 +56,7 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
|
||||||
jsonObj(c, allSetting, nil)
|
jsonObj(c, allSetting, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDefaultSettings retrieves the default settings based on the host.
|
||||||
func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
||||||
result, err := a.settingService.GetDefaultSettings(c.Request.Host)
|
result, err := a.settingService.GetDefaultSettings(c.Request.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -60,6 +66,7 @@ func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
||||||
jsonObj(c, result, nil)
|
jsonObj(c, result, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateSetting updates all settings with the provided data.
|
||||||
func (a *SettingController) updateSetting(c *gin.Context) {
|
func (a *SettingController) updateSetting(c *gin.Context) {
|
||||||
allSetting := &entity.AllSetting{}
|
allSetting := &entity.AllSetting{}
|
||||||
err := c.ShouldBind(allSetting)
|
err := c.ShouldBind(allSetting)
|
||||||
|
@ -71,6 +78,7 @@ func (a *SettingController) updateSetting(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateUser updates the current user's username and password.
|
||||||
func (a *SettingController) updateUser(c *gin.Context) {
|
func (a *SettingController) updateUser(c *gin.Context) {
|
||||||
form := &updateUserForm{}
|
form := &updateUserForm{}
|
||||||
err := c.ShouldBind(form)
|
err := c.ShouldBind(form)
|
||||||
|
@ -96,11 +104,13 @@ func (a *SettingController) updateUser(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// restartPanel restarts the panel service after a delay.
|
||||||
func (a *SettingController) restartPanel(c *gin.Context) {
|
func (a *SettingController) restartPanel(c *gin.Context) {
|
||||||
err := a.panelService.RestartPanel(time.Second * 3)
|
err := a.panelService.RestartPanel(time.Second * 3)
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.restartPanelSuccess"), err)
|
jsonMsg(c, I18nWeb(c, "pages.settings.restartPanelSuccess"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDefaultXrayConfig retrieves the default Xray configuration.
|
||||||
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
|
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
|
||||||
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
|
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// getRemoteIp extracts the real IP address from the request headers or remote address.
|
||||||
func getRemoteIp(c *gin.Context) string {
|
func getRemoteIp(c *gin.Context) string {
|
||||||
value := c.GetHeader("X-Real-IP")
|
value := c.GetHeader("X-Real-IP")
|
||||||
if value != "" {
|
if value != "" {
|
||||||
|
@ -27,14 +28,17 @@ func getRemoteIp(c *gin.Context) string {
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jsonMsg sends a JSON response with a message and error status.
|
||||||
func jsonMsg(c *gin.Context, msg string, err error) {
|
func jsonMsg(c *gin.Context, msg string, err error) {
|
||||||
jsonMsgObj(c, msg, nil, err)
|
jsonMsgObj(c, msg, nil, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jsonObj sends a JSON response with an object and error status.
|
||||||
func jsonObj(c *gin.Context, obj any, err error) {
|
func jsonObj(c *gin.Context, obj any, err error) {
|
||||||
jsonMsgObj(c, "", obj, err)
|
jsonMsgObj(c, "", obj, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jsonMsgObj sends a JSON response with a message, object, and error status.
|
||||||
func jsonMsgObj(c *gin.Context, msg string, obj any, err error) {
|
func jsonMsgObj(c *gin.Context, msg string, obj any, err error) {
|
||||||
m := entity.Msg{
|
m := entity.Msg{
|
||||||
Obj: obj,
|
Obj: obj,
|
||||||
|
@ -52,6 +56,7 @@ func jsonMsgObj(c *gin.Context, msg string, obj any, err error) {
|
||||||
c.JSON(http.StatusOK, m)
|
c.JSON(http.StatusOK, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pureJsonMsg sends a pure JSON message response with custom status code.
|
||||||
func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) {
|
func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) {
|
||||||
c.JSON(statusCode, entity.Msg{
|
c.JSON(statusCode, entity.Msg{
|
||||||
Success: success,
|
Success: success,
|
||||||
|
@ -59,6 +64,7 @@ func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// html renders an HTML template with the provided data and title.
|
||||||
func html(c *gin.Context, name string, title string, data gin.H) {
|
func html(c *gin.Context, name string, title string, data gin.H) {
|
||||||
if data == nil {
|
if data == nil {
|
||||||
data = gin.H{}
|
data = gin.H{}
|
||||||
|
@ -81,6 +87,7 @@ func html(c *gin.Context, name string, title string, data gin.H) {
|
||||||
c.HTML(http.StatusOK, name, getContext(data))
|
c.HTML(http.StatusOK, name, getContext(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getContext adds version and other context data to the provided gin.H.
|
||||||
func getContext(h gin.H) gin.H {
|
func getContext(h gin.H) gin.H {
|
||||||
a := gin.H{
|
a := gin.H{
|
||||||
"cur_ver": config.GetVersion(),
|
"cur_ver": config.GetVersion(),
|
||||||
|
@ -91,6 +98,7 @@ func getContext(h gin.H) gin.H {
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isAjax checks if the request is an AJAX request.
|
||||||
func isAjax(c *gin.Context) bool {
|
func isAjax(c *gin.Context) bool {
|
||||||
return c.GetHeader("X-Requested-With") == "XMLHttpRequest"
|
return c.GetHeader("X-Requested-With") == "XMLHttpRequest"
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// XraySettingController handles Xray configuration and settings operations.
|
||||||
type XraySettingController struct {
|
type XraySettingController struct {
|
||||||
XraySettingService service.XraySettingService
|
XraySettingService service.XraySettingService
|
||||||
SettingService service.SettingService
|
SettingService service.SettingService
|
||||||
|
@ -15,12 +16,14 @@ type XraySettingController struct {
|
||||||
WarpService service.WarpService
|
WarpService service.WarpService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewXraySettingController creates a new XraySettingController and initializes its routes.
|
||||||
func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
|
func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
|
||||||
a := &XraySettingController{}
|
a := &XraySettingController{}
|
||||||
a.initRouter(g)
|
a.initRouter(g)
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initRouter sets up the routes for Xray settings management.
|
||||||
func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||||
g = g.Group("/xray")
|
g = g.Group("/xray")
|
||||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||||
|
@ -33,6 +36,7 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||||
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getXraySetting retrieves the Xray configuration template and inbound tags.
|
||||||
func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
||||||
xraySetting, err := a.SettingService.GetXrayConfigTemplate()
|
xraySetting, err := a.SettingService.GetXrayConfigTemplate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -48,12 +52,14 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
||||||
jsonObj(c, xrayResponse, nil)
|
jsonObj(c, xrayResponse, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateSetting updates the Xray configuration settings.
|
||||||
func (a *XraySettingController) updateSetting(c *gin.Context) {
|
func (a *XraySettingController) updateSetting(c *gin.Context) {
|
||||||
xraySetting := c.PostForm("xraySetting")
|
xraySetting := c.PostForm("xraySetting")
|
||||||
err := a.XraySettingService.SaveXraySetting(xraySetting)
|
err := a.XraySettingService.SaveXraySetting(xraySetting)
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDefaultXrayConfig retrieves the default Xray configuration.
|
||||||
func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
|
func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
|
||||||
defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig()
|
defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -63,10 +69,12 @@ func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
|
||||||
jsonObj(c, defaultJsonConfig, nil)
|
jsonObj(c, defaultJsonConfig, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getXrayResult retrieves the current Xray service result.
|
||||||
func (a *XraySettingController) getXrayResult(c *gin.Context) {
|
func (a *XraySettingController) getXrayResult(c *gin.Context) {
|
||||||
jsonObj(c, a.XrayService.GetXrayResult(), nil)
|
jsonObj(c, a.XrayService.GetXrayResult(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// warp handles Warp-related operations based on the action parameter.
|
||||||
func (a *XraySettingController) warp(c *gin.Context) {
|
func (a *XraySettingController) warp(c *gin.Context) {
|
||||||
action := c.Param("action")
|
action := c.Param("action")
|
||||||
var resp string
|
var resp string
|
||||||
|
@ -90,6 +98,7 @@ func (a *XraySettingController) warp(c *gin.Context) {
|
||||||
jsonObj(c, resp, err)
|
jsonObj(c, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getOutboundsTraffic retrieves the traffic statistics for outbounds.
|
||||||
func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
|
func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
|
||||||
outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
|
outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -99,6 +108,7 @@ func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
|
||||||
jsonObj(c, outboundsTraffic, nil)
|
jsonObj(c, outboundsTraffic, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resetOutboundsTraffic resets the traffic statistics for the specified outbound tag.
|
||||||
func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
|
func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
|
||||||
tag := c.PostForm("tag")
|
tag := c.PostForm("tag")
|
||||||
err := a.OutboundService.ResetOutboundTraffic(tag)
|
err := a.OutboundService.ResetOutboundTraffic(tag)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// XUIController is the main controller for the X-UI panel, managing sub-controllers.
|
||||||
type XUIController struct {
|
type XUIController struct {
|
||||||
BaseController
|
BaseController
|
||||||
|
|
||||||
|
@ -13,12 +14,14 @@ type XUIController struct {
|
||||||
xraySettingController *XraySettingController
|
xraySettingController *XraySettingController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewXUIController creates a new XUIController and initializes its routes.
|
||||||
func NewXUIController(g *gin.RouterGroup) *XUIController {
|
func NewXUIController(g *gin.RouterGroup) *XUIController {
|
||||||
a := &XUIController{}
|
a := &XUIController{}
|
||||||
a.initRouter(g)
|
a.initRouter(g)
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initRouter sets up the main panel routes and initializes sub-controllers.
|
||||||
func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||||
g = g.Group("/panel")
|
g = g.Group("/panel")
|
||||||
g.Use(a.checkLogin)
|
g.Use(a.checkLogin)
|
||||||
|
@ -35,18 +38,22 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||||
a.xraySettingController = NewXraySettingController(g)
|
a.xraySettingController = NewXraySettingController(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// index renders the main panel index page.
|
||||||
func (a *XUIController) index(c *gin.Context) {
|
func (a *XUIController) index(c *gin.Context) {
|
||||||
html(c, "index.html", "pages.index.title", nil)
|
html(c, "index.html", "pages.index.title", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inbounds renders the inbounds management page.
|
||||||
func (a *XUIController) inbounds(c *gin.Context) {
|
func (a *XUIController) inbounds(c *gin.Context) {
|
||||||
html(c, "inbounds.html", "pages.inbounds.title", nil)
|
html(c, "inbounds.html", "pages.inbounds.title", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// settings renders the settings management page.
|
||||||
func (a *XUIController) settings(c *gin.Context) {
|
func (a *XUIController) settings(c *gin.Context) {
|
||||||
html(c, "settings.html", "pages.settings.title", nil)
|
html(c, "settings.html", "pages.settings.title", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// xraySettings renders the Xray settings page.
|
||||||
func (a *XUIController) xraySettings(c *gin.Context) {
|
func (a *XUIController) xraySettings(c *gin.Context) {
|
||||||
html(c, "xray.html", "pages.xray.title", nil)
|
html(c, "xray.html", "pages.xray.title", nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Package entity defines data structures and entities used by the web layer of the 3x-ui panel.
|
||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -10,61 +11,73 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Msg represents a standard API response message with success status, message text, and optional data object.
|
||||||
type Msg struct {
|
type Msg struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"` // Indicates if the operation was successful
|
||||||
Msg string `json:"msg"`
|
Msg string `json:"msg"` // Response message text
|
||||||
Obj any `json:"obj"`
|
Obj any `json:"obj"` // Optional data object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AllSetting contains all configuration settings for the 3x-ui panel including web server, Telegram bot, and subscription settings.
|
||||||
type AllSetting struct {
|
type AllSetting struct {
|
||||||
WebListen string `json:"webListen" form:"webListen"`
|
// Web server settings
|
||||||
WebDomain string `json:"webDomain" form:"webDomain"`
|
WebListen string `json:"webListen" form:"webListen"` // Web server listen IP address
|
||||||
WebPort int `json:"webPort" form:"webPort"`
|
WebDomain string `json:"webDomain" form:"webDomain"` // Web server domain for domain validation
|
||||||
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
WebPort int `json:"webPort" form:"webPort"` // Web server port number
|
||||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
WebCertFile string `json:"webCertFile" form:"webCertFile"` // Path to SSL certificate file for web server
|
||||||
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` // Path to SSL private key file for web server
|
||||||
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
|
WebBasePath string `json:"webBasePath" form:"webBasePath"` // Base path for web panel URLs
|
||||||
PageSize int `json:"pageSize" form:"pageSize"`
|
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"` // Session maximum age in minutes
|
||||||
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
|
|
||||||
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
|
// UI settings
|
||||||
RemarkModel string `json:"remarkModel" form:"remarkModel"`
|
PageSize int `json:"pageSize" form:"pageSize"` // Number of items per page in lists
|
||||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
ExpireDiff int `json:"expireDiff" form:"expireDiff"` // Expiration warning threshold in days
|
||||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"` // Traffic warning threshold percentage
|
||||||
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"`
|
RemarkModel string `json:"remarkModel" form:"remarkModel"` // Remark model pattern for inbounds
|
||||||
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"`
|
Datepicker string `json:"datepicker" form:"datepicker"` // Date picker format
|
||||||
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
|
|
||||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
// Telegram bot settings
|
||||||
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
|
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` // Enable Telegram bot notifications
|
||||||
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
|
TgBotToken string `json:"tgBotToken" form:"tgBotToken"` // Telegram bot token
|
||||||
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"` // Proxy URL for Telegram bot
|
||||||
TgLang string `json:"tgLang" form:"tgLang"`
|
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"` // Custom API server for Telegram bot
|
||||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"` // Telegram chat ID for notifications
|
||||||
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
TgRunTime string `json:"tgRunTime" form:"tgRunTime"` // Cron schedule for Telegram notifications
|
||||||
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` // Enable database backup via Telegram
|
||||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"` // Send login notifications
|
||||||
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"`
|
TgCpu int `json:"tgCpu" form:"tgCpu"` // CPU usage threshold for alerts
|
||||||
SubTitle string `json:"subTitle" form:"subTitle"`
|
TgLang string `json:"tgLang" form:"tgLang"` // Telegram bot language
|
||||||
SubListen string `json:"subListen" form:"subListen"`
|
|
||||||
SubPort int `json:"subPort" form:"subPort"`
|
// Security settings
|
||||||
SubPath string `json:"subPath" form:"subPath"`
|
TimeLocation string `json:"timeLocation" form:"timeLocation"` // Time zone location
|
||||||
SubDomain string `json:"subDomain" form:"subDomain"`
|
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"` // Enable two-factor authentication
|
||||||
SubCertFile string `json:"subCertFile" form:"subCertFile"`
|
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"` // Two-factor authentication token
|
||||||
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
|
|
||||||
SubUpdates int `json:"subUpdates" form:"subUpdates"`
|
// Subscription server settings
|
||||||
ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"`
|
SubEnable bool `json:"subEnable" form:"subEnable"` // Enable subscription server
|
||||||
ExternalTrafficInformURI string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"`
|
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"` // Enable JSON subscription endpoint
|
||||||
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
|
SubTitle string `json:"subTitle" form:"subTitle"` // Subscription title
|
||||||
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
|
SubListen string `json:"subListen" form:"subListen"` // Subscription server listen IP
|
||||||
SubURI string `json:"subURI" form:"subURI"`
|
SubPort int `json:"subPort" form:"subPort"` // Subscription server port
|
||||||
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
|
SubPath string `json:"subPath" form:"subPath"` // Base path for subscription URLs
|
||||||
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
|
SubDomain string `json:"subDomain" form:"subDomain"` // Domain for subscription server validation
|
||||||
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
|
SubCertFile string `json:"subCertFile" form:"subCertFile"` // SSL certificate file for subscription server
|
||||||
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"`
|
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"` // SSL private key file for subscription server
|
||||||
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
|
SubUpdates int `json:"subUpdates" form:"subUpdates"` // Subscription update interval in minutes
|
||||||
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
|
ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"` // Enable external traffic reporting
|
||||||
Datepicker string `json:"datepicker" form:"datepicker"`
|
ExternalTrafficInformURI string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"` // URI for external traffic reporting
|
||||||
|
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"` // Encrypt subscription responses
|
||||||
|
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"` // Show client information in subscriptions
|
||||||
|
SubURI string `json:"subURI" form:"subURI"` // Subscription server URI
|
||||||
|
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"` // Path for JSON subscription endpoint
|
||||||
|
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"` // JSON subscription server URI
|
||||||
|
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"` // JSON subscription fragment configuration
|
||||||
|
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"` // JSON subscription noise configuration
|
||||||
|
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` // JSON subscription mux configuration
|
||||||
|
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"` // JSON subscription routing rules
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckValid validates all settings in the AllSetting struct, checking IP addresses, ports, SSL certificates, and other configuration values.
|
||||||
func (s *AllSetting) CheckValid() error {
|
func (s *AllSetting) CheckValid() error {
|
||||||
if s.WebListen != "" {
|
if s.WebListen != "" {
|
||||||
ip := net.ParseIP(s.WebListen)
|
ip := net.ParseIP(s.WebListen)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Package global provides global variables and interfaces for accessing web and subscription servers.
|
||||||
package global
|
package global
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -12,27 +13,33 @@ var (
|
||||||
subServer SubServer
|
subServer SubServer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WebServer interface defines methods for accessing the web server instance.
|
||||||
type WebServer interface {
|
type WebServer interface {
|
||||||
GetCron() *cron.Cron
|
GetCron() *cron.Cron // Get the cron scheduler
|
||||||
GetCtx() context.Context
|
GetCtx() context.Context // Get the server context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubServer interface defines methods for accessing the subscription server instance.
|
||||||
type SubServer interface {
|
type SubServer interface {
|
||||||
GetCtx() context.Context
|
GetCtx() context.Context // Get the server context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetWebServer sets the global web server instance.
|
||||||
func SetWebServer(s WebServer) {
|
func SetWebServer(s WebServer) {
|
||||||
webServer = s
|
webServer = s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetWebServer returns the global web server instance.
|
||||||
func GetWebServer() WebServer {
|
func GetWebServer() WebServer {
|
||||||
return webServer
|
return webServer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSubServer sets the global subscription server instance.
|
||||||
func SetSubServer(s SubServer) {
|
func SetSubServer(s SubServer) {
|
||||||
subServer = s
|
subServer = s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSubServer returns the global subscription server instance.
|
||||||
func GetSubServer() SubServer {
|
func GetSubServer() SubServer {
|
||||||
return subServer
|
return subServer
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,18 +8,21 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// HashEntry represents a stored hash entry with its value and timestamp.
|
||||||
type HashEntry struct {
|
type HashEntry struct {
|
||||||
Hash string
|
Hash string // MD5 hash string
|
||||||
Value string
|
Value string // Original value
|
||||||
Timestamp time.Time
|
Timestamp time.Time // Time when the hash was created
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HashStorage provides thread-safe storage for hash-value pairs with expiration.
|
||||||
type HashStorage struct {
|
type HashStorage struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
Data map[string]HashEntry
|
Data map[string]HashEntry // Map of hash to entry
|
||||||
Expiration time.Duration
|
Expiration time.Duration // Expiration duration for entries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewHashStorage creates a new HashStorage instance with the specified expiration duration.
|
||||||
func NewHashStorage(expiration time.Duration) *HashStorage {
|
func NewHashStorage(expiration time.Duration) *HashStorage {
|
||||||
return &HashStorage{
|
return &HashStorage{
|
||||||
Data: make(map[string]HashEntry),
|
Data: make(map[string]HashEntry),
|
||||||
|
@ -27,6 +30,7 @@ func NewHashStorage(expiration time.Duration) *HashStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveHash generates an MD5 hash for the given query string and stores it with a timestamp.
|
||||||
func (h *HashStorage) SaveHash(query string) string {
|
func (h *HashStorage) SaveHash(query string) string {
|
||||||
h.Lock()
|
h.Lock()
|
||||||
defer h.Unlock()
|
defer h.Unlock()
|
||||||
|
@ -45,6 +49,7 @@ func (h *HashStorage) SaveHash(query string) string {
|
||||||
return md5HashString
|
return md5HashString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetValue retrieves the original value for the given hash, returning true if found.
|
||||||
func (h *HashStorage) GetValue(hash string) (string, bool) {
|
func (h *HashStorage) GetValue(hash string) (string, bool) {
|
||||||
h.RLock()
|
h.RLock()
|
||||||
defer h.RUnlock()
|
defer h.RUnlock()
|
||||||
|
@ -54,11 +59,13 @@ func (h *HashStorage) GetValue(hash string) (string, bool) {
|
||||||
return entry.Value, exists
|
return entry.Value, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsMD5 checks if the given string is a valid 32-character MD5 hash.
|
||||||
func (h *HashStorage) IsMD5(hash string) bool {
|
func (h *HashStorage) IsMD5(hash string) bool {
|
||||||
match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash)
|
match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash)
|
||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveExpiredHashes removes all hash entries that have exceeded the expiration duration.
|
||||||
func (h *HashStorage) RemoveExpiredHashes() {
|
func (h *HashStorage) RemoveExpiredHashes() {
|
||||||
h.Lock()
|
h.Lock()
|
||||||
defer h.Unlock()
|
defer h.Unlock()
|
||||||
|
@ -72,6 +79,7 @@ func (h *HashStorage) RemoveExpiredHashes() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset clears all stored hash entries.
|
||||||
func (h *HashStorage) Reset() {
|
func (h *HashStorage) Reset() {
|
||||||
h.Lock()
|
h.Lock()
|
||||||
defer h.Unlock()
|
defer h.Unlock()
|
||||||
|
|
|
@ -2,21 +2,21 @@
|
||||||
<template slot="actions" slot-scope="text, client, index">
|
<template slot="actions" slot-scope="text, client, index">
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">{{ i18n "qrCode" }}</template>
|
<template slot="title">{{ i18n "qrCode" }}</template>
|
||||||
<a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
|
<a-icon :style="{ fontSize: '22px', marginInlineStart: '14px' }" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
||||||
<a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
|
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">{{ i18n "info" }}</template>
|
<template slot="title">{{ i18n "info" }}</template>
|
||||||
<a-icon :style="{ fontSize: '24px' }" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
|
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||||
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
|
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
|
||||||
<a-icon slot="icon" type="question-circle-o" :style="{ color: 'var(--color-primary-100)'}"></a-icon>
|
<a-icon slot="icon" type="question-circle-o" :style="{ color: 'var(--color-primary-100)'}"></a-icon>
|
||||||
<a-icon :style="{ fontSize: '24px', cursor: 'pointer' }" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
|
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
</template>
|
</template>
|
||||||
<a-popconfirm @confirm="delClient(record.id,client,false)" title='{{ i18n "pages.inbounds.deleteClientContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "delete"}}' ok-type="danger" cancel-text='{{ i18n "cancel"}}'>
|
<a-popconfirm @confirm="delClient(record.id,client,false)" title='{{ i18n "pages.inbounds.deleteClientContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "delete"}}' ok-type="danger" cancel-text='{{ i18n "cancel"}}'>
|
||||||
<a-icon slot="icon" type="question-circle-o" :style="{ color: '#e04141' }"></a-icon>
|
<a-icon slot="icon" type="question-circle-o" :style="{ color: '#e04141' }"></a-icon>
|
||||||
<a-icon :style="{ fontSize: '24px', cursor: 'pointer' }" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
|
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -660,7 +660,7 @@
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.allTimeTraffic" }}',
|
title: '{{ i18n "pages.inbounds.allTimeTraffic" }}',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 70,
|
width: 60,
|
||||||
scopedSlots: { customRender: 'allTimeInbound' },
|
scopedSlots: { customRender: 'allTimeInbound' },
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
||||||
|
@ -693,12 +693,12 @@
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const innerColumns = [
|
const innerColumns = [
|
||||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 65, scopedSlots: { customRender: 'actions' } },
|
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 35, scopedSlots: { customRender: 'enable' } },
|
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
|
||||||
{ title: '{{ i18n "online" }}', width: 32, scopedSlots: { customRender: 'online' } },
|
{ title: '{{ i18n "online" }}', width: 32, scopedSlots: { customRender: 'online' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
|
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },
|
{ title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 60, align: 'center', scopedSlots: { customRender: 'allTime' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -736,7 +736,7 @@
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||||
subSettings: {
|
subSettings: {
|
||||||
enable: true,
|
enable: false,
|
||||||
subTitle: '',
|
subTitle: '',
|
||||||
subURI: '',
|
subURI: '',
|
||||||
subJsonURI: '',
|
subJsonURI: '',
|
||||||
|
@ -747,7 +747,7 @@
|
||||||
tgBotEnable: false,
|
tgBotEnable: false,
|
||||||
showAlert: false,
|
showAlert: false,
|
||||||
ipLimitEnable: false,
|
ipLimitEnable: false,
|
||||||
pageSize: 50,
|
pageSize: 0,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loading(spinning = true) {
|
loading(spinning = true) {
|
||||||
|
|
|
@ -3,22 +3,29 @@
|
||||||
:mask-closable="false" :footer="null" :class="themeSwitcher.currentTheme">
|
:mask-closable="false" :footer="null" :class="themeSwitcher.currentTheme">
|
||||||
<a-list class="ant-dns-presets-list" bordered :style="{ width: '100%' }">
|
<a-list class="ant-dns-presets-list" bordered :style="{ width: '100%' }">
|
||||||
<a-list-item v-for="dns in dnsPresetsDatabase" :style="{ padding: '12px 16px' }">
|
<a-list-item v-for="dns in dnsPresetsDatabase" :style="{ padding: '12px 16px' }">
|
||||||
<a-row justify="space-between" align="middle">
|
<div class="ant-dns-presets-line">
|
||||||
<a-col :span="12">
|
<a-space direction="horizontal" size="small" align="center">
|
||||||
<a-space direction="vertical" size="small">
|
|
||||||
<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-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-space>
|
||||||
</a-col>
|
<a-button class="ant-dns-presets-install" type="primary" @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
|
||||||
<a-col :span="12" :style="{ textAlign: 'right' }">
|
</div>
|
||||||
<a-button type="primary" @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</a-list-item>
|
</a-list-item>
|
||||||
</a-list>
|
</a-list>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.ant-dns-presets-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dns-presets-install {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.dark .ant-dns-presets-list {
|
.dark .ant-dns-presets-list {
|
||||||
border-color: var(--dark-color-stroke)
|
border-color: var(--dark-color-stroke)
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,7 +106,10 @@
|
||||||
<a-tag v-else color="red">{{ i18n "none" }}</a-tag>
|
<a-tag v-else color="red">{{ i18n "none" }}</a-tag>
|
||||||
<br />
|
<br />
|
||||||
{{ i18n "encryption" }}
|
{{ i18n "encryption" }}
|
||||||
<a-tag :color="inbound.settings.encryption ? 'green' : 'red'">[[ inbound.settings.encryption ? inbound.settings.encryption : '' ]]</a-tag>
|
<a-tag class="info-large-tag" :color="inbound.settings.encryption ? 'green' : 'red'">[[ inbound.settings.encryption ? inbound.settings.encryption : '' ]]</a-tag>
|
||||||
|
<a-tooltip title='{{ i18n "copy" }}'>
|
||||||
|
<a-button size="small" icon="snippets" @click="copy(inbound.settings.encryption)"></a-button>
|
||||||
|
</a-tooltip>
|
||||||
<br />
|
<br />
|
||||||
<template v-if="inbound.stream.security != 'none'">
|
<template v-if="inbound.stream.security != 'none'">
|
||||||
{{ i18n "domainName" }}
|
{{ i18n "domainName" }}
|
||||||
|
|
|
@ -48,7 +48,13 @@
|
||||||
<template #title>{{ i18n "pages.settings.subPath"}}</template>
|
<template #title>{{ i18n "pages.settings.subPath"}}</template>
|
||||||
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
|
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-input type="text" v-model="allSetting.subPath"></a-input>
|
<a-input
|
||||||
|
type="text"
|
||||||
|
v-model="allSetting.subPath"
|
||||||
|
@input="allSetting.subPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')"
|
||||||
|
@blur="allSetting.subPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subPath)"
|
||||||
|
placeholder="/sub/"
|
||||||
|
></a-input>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
|
|
|
@ -5,7 +5,13 @@
|
||||||
<template #title>{{ i18n "pages.settings.subPath"}}</template>
|
<template #title>{{ i18n "pages.settings.subPath"}}</template>
|
||||||
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
|
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-input type="text" v-model="allSetting.subJsonPath"></a-input>
|
<a-input
|
||||||
|
type="text"
|
||||||
|
v-model="allSetting.subJsonPath"
|
||||||
|
@input="allSetting.subJsonPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')"
|
||||||
|
@blur="allSetting.subJsonPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subJsonPath)"
|
||||||
|
placeholder="/json/"
|
||||||
|
></a-input>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
|
|
|
@ -534,13 +534,11 @@
|
||||||
serverObj = null;
|
serverObj = null;
|
||||||
switch (o.protocol) {
|
switch (o.protocol) {
|
||||||
case Protocols.VMess:
|
case Protocols.VMess:
|
||||||
case Protocols.VLESS:
|
serverObj = o.settings.vnext;
|
||||||
if (o.settings && o.settings.address && o.settings.port) {
|
|
||||||
return [o.settings.address + ':' + o.settings.port];
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
case Protocols.VLESS:
|
||||||
case Protocols.HTTP:
|
case Protocols.HTTP:
|
||||||
case Protocols.Mixed:
|
case Protocols.Socks:
|
||||||
case Protocols.Shadowsocks:
|
case Protocols.Shadowsocks:
|
||||||
case Protocols.Trojan:
|
case Protocols.Trojan:
|
||||||
serverObj = o.settings.servers;
|
serverObj = o.settings.servers;
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CheckClientIpJob monitors client IP addresses from access logs and manages IP blocking based on configured limits.
|
||||||
type CheckClientIpJob struct {
|
type CheckClientIpJob struct {
|
||||||
lastClear int64
|
lastClear int64
|
||||||
disAllowedIps []string
|
disAllowedIps []string
|
||||||
|
@ -25,6 +26,7 @@ type CheckClientIpJob struct {
|
||||||
|
|
||||||
var job *CheckClientIpJob
|
var job *CheckClientIpJob
|
||||||
|
|
||||||
|
// NewCheckClientIpJob creates a new client IP monitoring job instance.
|
||||||
func NewCheckClientIpJob() *CheckClientIpJob {
|
func NewCheckClientIpJob() *CheckClientIpJob {
|
||||||
job = new(CheckClientIpJob)
|
job = new(CheckClientIpJob)
|
||||||
return job
|
return job
|
||||||
|
|
|
@ -9,16 +9,18 @@ import (
|
||||||
"github.com/shirou/gopsutil/v4/cpu"
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CheckCpuJob monitors CPU usage and sends Telegram notifications when usage exceeds the configured threshold.
|
||||||
type CheckCpuJob struct {
|
type CheckCpuJob struct {
|
||||||
tgbotService service.Tgbot
|
tgbotService service.Tgbot
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCheckCpuJob creates a new CPU monitoring job instance.
|
||||||
func NewCheckCpuJob() *CheckCpuJob {
|
func NewCheckCpuJob() *CheckCpuJob {
|
||||||
return new(CheckCpuJob)
|
return new(CheckCpuJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here run is a interface method of Job interface
|
// Run checks CPU usage over the last minute and sends a Telegram alert if it exceeds the threshold.
|
||||||
func (j *CheckCpuJob) Run() {
|
func (j *CheckCpuJob) Run() {
|
||||||
threshold, _ := j.settingService.GetTgCpu()
|
threshold, _ := j.settingService.GetTgCpu()
|
||||||
|
|
||||||
|
|
|
@ -4,15 +4,17 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CheckHashStorageJob periodically cleans up expired hash entries from the Telegram bot's hash storage.
|
||||||
type CheckHashStorageJob struct {
|
type CheckHashStorageJob struct {
|
||||||
tgbotService service.Tgbot
|
tgbotService service.Tgbot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCheckHashStorageJob creates a new hash storage cleanup job instance.
|
||||||
func NewCheckHashStorageJob() *CheckHashStorageJob {
|
func NewCheckHashStorageJob() *CheckHashStorageJob {
|
||||||
return new(CheckHashStorageJob)
|
return new(CheckHashStorageJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here Run is an interface method of the Job interface
|
// Run removes expired hash entries from the Telegram bot's hash storage.
|
||||||
func (j *CheckHashStorageJob) Run() {
|
func (j *CheckHashStorageJob) Run() {
|
||||||
// Remove expired hashes from storage
|
// Remove expired hashes from storage
|
||||||
j.tgbotService.GetHashStorage().RemoveExpiredHashes()
|
j.tgbotService.GetHashStorage().RemoveExpiredHashes()
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package job provides background job implementations for the 3x-ui web panel,
|
||||||
|
// including traffic monitoring, system checks, and periodic maintenance tasks.
|
||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -5,16 +7,18 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CheckXrayRunningJob monitors Xray process health and restarts it if it crashes.
|
||||||
type CheckXrayRunningJob struct {
|
type CheckXrayRunningJob struct {
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
|
|
||||||
checkTime int
|
checkTime int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCheckXrayRunningJob creates a new Xray health check job instance.
|
||||||
func NewCheckXrayRunningJob() *CheckXrayRunningJob {
|
func NewCheckXrayRunningJob() *CheckXrayRunningJob {
|
||||||
return new(CheckXrayRunningJob)
|
return new(CheckXrayRunningJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run checks if Xray has crashed and restarts it after confirming it's down for 2 consecutive checks.
|
||||||
func (j *CheckXrayRunningJob) Run() {
|
func (j *CheckXrayRunningJob) Run() {
|
||||||
if !j.xrayService.DidXrayCrash() {
|
if !j.xrayService.DidXrayCrash() {
|
||||||
j.checkTime = 0
|
j.checkTime = 0
|
||||||
|
|
|
@ -9,8 +9,10 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ClearLogsJob clears old log files to prevent disk space issues.
|
||||||
type ClearLogsJob struct{}
|
type ClearLogsJob struct{}
|
||||||
|
|
||||||
|
// NewClearLogsJob creates a new log cleanup job instance.
|
||||||
func NewClearLogsJob() *ClearLogsJob {
|
func NewClearLogsJob() *ClearLogsJob {
|
||||||
return new(ClearLogsJob)
|
return new(ClearLogsJob)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,19 +5,23 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Period represents the time period for traffic resets.
|
||||||
type Period string
|
type Period string
|
||||||
|
|
||||||
|
// PeriodicTrafficResetJob resets traffic statistics for inbounds based on their configured reset period.
|
||||||
type PeriodicTrafficResetJob struct {
|
type PeriodicTrafficResetJob struct {
|
||||||
inboundService service.InboundService
|
inboundService service.InboundService
|
||||||
period Period
|
period Period
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewPeriodicTrafficResetJob creates a new periodic traffic reset job for the specified period.
|
||||||
func NewPeriodicTrafficResetJob(period Period) *PeriodicTrafficResetJob {
|
func NewPeriodicTrafficResetJob(period Period) *PeriodicTrafficResetJob {
|
||||||
return &PeriodicTrafficResetJob{
|
return &PeriodicTrafficResetJob{
|
||||||
period: period,
|
period: period,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run resets traffic statistics for all inbounds that match the configured reset period.
|
||||||
func (j *PeriodicTrafficResetJob) Run() {
|
func (j *PeriodicTrafficResetJob) Run() {
|
||||||
inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))
|
inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -4,23 +4,26 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LoginStatus represents the status of a login attempt.
|
||||||
type LoginStatus byte
|
type LoginStatus byte
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LoginSuccess LoginStatus = 1
|
LoginSuccess LoginStatus = 1 // Successful login
|
||||||
LoginFail LoginStatus = 0
|
LoginFail LoginStatus = 0 // Failed login attempt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// StatsNotifyJob sends periodic statistics reports via Telegram bot.
|
||||||
type StatsNotifyJob struct {
|
type StatsNotifyJob struct {
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
tgbotService service.Tgbot
|
tgbotService service.Tgbot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewStatsNotifyJob creates a new statistics notification job instance.
|
||||||
func NewStatsNotifyJob() *StatsNotifyJob {
|
func NewStatsNotifyJob() *StatsNotifyJob {
|
||||||
return new(StatsNotifyJob)
|
return new(StatsNotifyJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here run is a interface method of Job interface
|
// Run sends a statistics report via Telegram bot if Xray is running.
|
||||||
func (j *StatsNotifyJob) Run() {
|
func (j *StatsNotifyJob) Run() {
|
||||||
if !j.xrayService.IsXrayRunning() {
|
if !j.xrayService.IsXrayRunning() {
|
||||||
return
|
return
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// XrayTrafficJob collects and processes traffic statistics from Xray, updating the database and optionally informing external APIs.
|
||||||
type XrayTrafficJob struct {
|
type XrayTrafficJob struct {
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
|
@ -17,10 +18,12 @@ type XrayTrafficJob struct {
|
||||||
outboundService service.OutboundService
|
outboundService service.OutboundService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewXrayTrafficJob creates a new traffic collection job instance.
|
||||||
func NewXrayTrafficJob() *XrayTrafficJob {
|
func NewXrayTrafficJob() *XrayTrafficJob {
|
||||||
return new(XrayTrafficJob)
|
return new(XrayTrafficJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run collects traffic statistics from Xray and updates the database, triggering restart if needed.
|
||||||
func (j *XrayTrafficJob) Run() {
|
func (j *XrayTrafficJob) Run() {
|
||||||
if !j.xrayService.IsXrayRunning() {
|
if !j.xrayService.IsXrayRunning() {
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package locale provides internationalization (i18n) support for the 3x-ui web panel,
|
||||||
|
// including translation loading, localization, and middleware for web and bot interfaces.
|
||||||
package locale
|
package locale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -20,17 +22,20 @@ var (
|
||||||
LocalizerBot *i18n.Localizer
|
LocalizerBot *i18n.Localizer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// I18nType represents the type of interface for internationalization.
|
||||||
type I18nType string
|
type I18nType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Bot I18nType = "bot"
|
Bot I18nType = "bot" // Bot interface type
|
||||||
Web I18nType = "web"
|
Web I18nType = "web" // Web interface type
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SettingService interface defines methods for accessing locale settings.
|
||||||
type SettingService interface {
|
type SettingService interface {
|
||||||
GetTgLang() (string, error)
|
GetTgLang() (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitLocalizer initializes the internationalization system with embedded translation files.
|
||||||
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
||||||
// set default bundle to english
|
// set default bundle to english
|
||||||
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
|
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
|
||||||
|
@ -49,6 +54,7 @@ func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createTemplateData creates a template data map from parameters with optional separator.
|
||||||
func createTemplateData(params []string, separator ...string) map[string]any {
|
func createTemplateData(params []string, separator ...string) map[string]any {
|
||||||
var sep string = "=="
|
var sep string = "=="
|
||||||
if len(separator) > 0 {
|
if len(separator) > 0 {
|
||||||
|
@ -64,6 +70,9 @@ func createTemplateData(params []string, separator ...string) map[string]any {
|
||||||
return templateData
|
return templateData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// I18n retrieves a localized message for the given key and type.
|
||||||
|
// It supports both bot and web contexts, with optional template parameters.
|
||||||
|
// Returns the localized message or an empty string if localization fails.
|
||||||
func I18n(i18nType I18nType, key string, params ...string) string {
|
func I18n(i18nType I18nType, key string, params ...string) string {
|
||||||
var localizer *i18n.Localizer
|
var localizer *i18n.Localizer
|
||||||
|
|
||||||
|
@ -96,6 +105,7 @@ func I18n(i18nType I18nType, key string, params ...string) string {
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initTGBotLocalizer initializes the bot localizer with the configured language.
|
||||||
func initTGBotLocalizer(settingService SettingService) error {
|
func initTGBotLocalizer(settingService SettingService) error {
|
||||||
botLang, err := settingService.GetTgLang()
|
botLang, err := settingService.GetTgLang()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -106,6 +116,10 @@ func initTGBotLocalizer(settingService SettingService) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LocalizerMiddleware returns a Gin middleware that sets up localization for web requests.
|
||||||
|
// It determines the user's language from cookies or Accept-Language header,
|
||||||
|
// creates a localizer instance, and stores it in the Gin context for use in handlers.
|
||||||
|
// Also provides the I18n function in the context for template rendering.
|
||||||
func LocalizerMiddleware() gin.HandlerFunc {
|
func LocalizerMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// Ensure bundle is initialized so creating a Localizer won't panic
|
// Ensure bundle is initialized so creating a Localizer won't panic
|
||||||
|
@ -152,6 +166,7 @@ func loadTranslationsFromDisk(bundle *i18n.Bundle) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseTranslationFiles parses embedded translation files and adds them to the i18n bundle.
|
||||||
func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
|
func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
|
||||||
err := fs.WalkDir(i18nFS, "translation",
|
err := fs.WalkDir(i18nFS, "translation",
|
||||||
func(path string, d fs.DirEntry, err error) error {
|
func(path string, d fs.DirEntry, err error) error {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package middleware provides HTTP middleware functions for the 3x-ui web panel,
|
||||||
|
// including domain validation and URL redirection utilities.
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -8,6 +10,10 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DomainValidatorMiddleware returns a Gin middleware that validates the request domain.
|
||||||
|
// It extracts the host from the request, strips any port number, and compares it
|
||||||
|
// against the configured domain. Requests from unauthorized domains are rejected
|
||||||
|
// with HTTP 403 Forbidden status.
|
||||||
func DomainValidatorMiddleware(domain string) gin.HandlerFunc {
|
func DomainValidatorMiddleware(domain string) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
host := c.Request.Host
|
host := c.Request.Host
|
||||||
|
|
|
@ -7,6 +7,9 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RedirectMiddleware returns a Gin middleware that handles URL redirections.
|
||||||
|
// It provides backward compatibility by redirecting old '/xui' paths to new '/panel' paths,
|
||||||
|
// including API endpoints. The middleware performs permanent redirects (301) for SEO purposes.
|
||||||
func RedirectMiddleware(basePath string) gin.HandlerFunc {
|
func RedirectMiddleware(basePath string) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// Redirect from old '/xui' path to '/panel'
|
// Redirect from old '/xui' path to '/panel'
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package network provides network utilities for the 3x-ui web panel,
|
||||||
|
// including automatic HTTP to HTTPS redirection functionality.
|
||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -9,6 +11,9 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AutoHttpsConn wraps a net.Conn to provide automatic HTTP to HTTPS redirection.
|
||||||
|
// It intercepts the first read to detect HTTP requests and responds with a 307 redirect
|
||||||
|
// to the HTTPS equivalent URL. Subsequent reads work normally for HTTPS connections.
|
||||||
type AutoHttpsConn struct {
|
type AutoHttpsConn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
|
|
||||||
|
@ -18,6 +23,8 @@ type AutoHttpsConn struct {
|
||||||
readRequestOnce sync.Once
|
readRequestOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewAutoHttpsConn creates a new AutoHttpsConn that wraps the given connection.
|
||||||
|
// It enables automatic redirection of HTTP requests to HTTPS.
|
||||||
func NewAutoHttpsConn(conn net.Conn) net.Conn {
|
func NewAutoHttpsConn(conn net.Conn) net.Conn {
|
||||||
return &AutoHttpsConn{
|
return &AutoHttpsConn{
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
|
@ -49,6 +56,9 @@ func (c *AutoHttpsConn) readRequest() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read implements the net.Conn Read method with automatic HTTPS redirection.
|
||||||
|
// On the first read, it checks if the request is HTTP and redirects to HTTPS if so.
|
||||||
|
// Subsequent reads work normally.
|
||||||
func (c *AutoHttpsConn) Read(buf []byte) (int, error) {
|
func (c *AutoHttpsConn) Read(buf []byte) (int, error) {
|
||||||
c.readRequestOnce.Do(func() {
|
c.readRequestOnce.Do(func() {
|
||||||
c.readRequest()
|
c.readRequest()
|
||||||
|
|
|
@ -2,16 +2,22 @@ package network
|
||||||
|
|
||||||
import "net"
|
import "net"
|
||||||
|
|
||||||
|
// AutoHttpsListener wraps a net.Listener to provide automatic HTTPS redirection.
|
||||||
|
// It returns AutoHttpsConn connections that handle HTTP to HTTPS redirection.
|
||||||
type AutoHttpsListener struct {
|
type AutoHttpsListener struct {
|
||||||
net.Listener
|
net.Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewAutoHttpsListener creates a new AutoHttpsListener that wraps the given listener.
|
||||||
|
// It enables automatic redirection of HTTP requests to HTTPS for all accepted connections.
|
||||||
func NewAutoHttpsListener(listener net.Listener) net.Listener {
|
func NewAutoHttpsListener(listener net.Listener) net.Listener {
|
||||||
return &AutoHttpsListener{
|
return &AutoHttpsListener{
|
||||||
Listener: listener,
|
Listener: listener,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accept implements the net.Listener Accept method.
|
||||||
|
// It accepts connections and wraps them with AutoHttpsConn for HTTPS redirection.
|
||||||
func (l *AutoHttpsListener) Accept() (net.Conn, error) {
|
func (l *AutoHttpsListener) Accept() (net.Conn, error) {
|
||||||
conn, err := l.Listener.Accept()
|
conn, err := l.Listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package service provides business logic services for the 3x-ui web panel,
|
||||||
|
// including inbound/outbound management, user administration, settings, and Xray integration.
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -20,10 +22,15 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// InboundService provides business logic for managing Xray inbound configurations.
|
||||||
|
// It handles CRUD operations for inbounds, client management, traffic monitoring,
|
||||||
|
// and integration with the Xray API for real-time updates.
|
||||||
type InboundService struct {
|
type InboundService struct {
|
||||||
xrayApi xray.XrayAPI
|
xrayApi xray.XrayAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInbounds retrieves all inbounds for a specific user.
|
||||||
|
// Returns a slice of inbound models with their associated client statistics.
|
||||||
func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
|
func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var inbounds []*model.Inbound
|
var inbounds []*model.Inbound
|
||||||
|
@ -34,6 +41,8 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
|
||||||
return inbounds, nil
|
return inbounds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllInbounds retrieves all inbounds from the database.
|
||||||
|
// Returns a slice of all inbound models with their associated client statistics.
|
||||||
func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
|
func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var inbounds []*model.Inbound
|
var inbounds []*model.Inbound
|
||||||
|
@ -166,6 +175,10 @@ func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (stri
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddInbound creates a new inbound configuration.
|
||||||
|
// It validates port uniqueness, client email uniqueness, and required fields,
|
||||||
|
// then saves the inbound to the database and optionally adds it to the running Xray instance.
|
||||||
|
// Returns the created inbound, whether Xray needs restart, and any error.
|
||||||
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
||||||
exist, err := s.checkPortExist(inbound.Listen, inbound.Port, 0)
|
exist, err := s.checkPortExist(inbound.Listen, inbound.Port, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -272,6 +285,9 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||||
return inbound, needRestart, err
|
return inbound, needRestart, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DelInbound deletes an inbound configuration by ID.
|
||||||
|
// It removes the inbound from the database and the running Xray instance if active.
|
||||||
|
// Returns whether Xray needs restart and any error.
|
||||||
func (s *InboundService) DelInbound(id int) (bool, error) {
|
func (s *InboundService) DelInbound(id int) (bool, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
|
@ -325,6 +341,9 @@ func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
|
||||||
return inbound, nil
|
return inbound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateInbound modifies an existing inbound configuration.
|
||||||
|
// It validates changes, updates the database, and syncs with the running Xray instance.
|
||||||
|
// Returns the updated inbound, whether Xray needs restart, and any error.
|
||||||
func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
||||||
exist, err := s.checkPortExist(inbound.Listen, inbound.Port, inbound.Id)
|
exist, err := s.checkPortExist(inbound.Listen, inbound.Port, inbound.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1959,6 +1978,15 @@ func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffi
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Populate UUID and other client data for each traffic record
|
||||||
|
for i := range traffics {
|
||||||
|
if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil {
|
||||||
|
traffics[i].Enable = client.Enable
|
||||||
|
traffics[i].UUID = client.ID
|
||||||
|
traffics[i].SubId = client.SubID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return traffics, nil
|
return traffics, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1971,6 +1999,7 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
|
||||||
}
|
}
|
||||||
if t != nil && client != nil {
|
if t != nil && client != nil {
|
||||||
t.Enable = client.Enable
|
t.Enable = client.Enable
|
||||||
|
t.UUID = client.ID
|
||||||
t.SubId = client.SubID
|
t.SubId = client.SubID
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
@ -2012,6 +2041,7 @@ func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic,
|
||||||
for i := range traffics {
|
for i := range traffics {
|
||||||
if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil {
|
if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil {
|
||||||
traffics[i].Enable = client.Enable
|
traffics[i].Enable = client.Enable
|
||||||
|
traffics[i].UUID = client.ID
|
||||||
traffics[i].SubId = client.SubID
|
traffics[i].SubId = client.SubID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2110,6 +2140,9 @@ func (s *InboundService) MigrationRequirements() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
|
if dbErr := db.Exec(`VACUUM "main"`).Error; dbErr != nil {
|
||||||
|
logger.Warningf("VACUUM failed: %v", dbErr)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// OutboundService provides business logic for managing Xray outbound configurations.
|
||||||
|
// It handles outbound traffic monitoring and statistics.
|
||||||
type OutboundService struct{}
|
type OutboundService struct{}
|
||||||
|
|
||||||
func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
|
func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PanelService provides business logic for panel management operations.
|
||||||
|
// It handles panel restart, updates, and system-level panel controls.
|
||||||
type PanelService struct{}
|
type PanelService struct{}
|
||||||
|
|
||||||
func (s *PanelService) RestartPanel(delay time.Duration) error {
|
func (s *PanelService) RestartPanel(delay time.Duration) error {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -35,14 +36,18 @@ import (
|
||||||
"github.com/shirou/gopsutil/v4/net"
|
"github.com/shirou/gopsutil/v4/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ProcessState represents the current state of a system process.
|
||||||
type ProcessState string
|
type ProcessState string
|
||||||
|
|
||||||
|
// Process state constants
|
||||||
const (
|
const (
|
||||||
Running ProcessState = "running"
|
Running ProcessState = "running" // Process is running normally
|
||||||
Stop ProcessState = "stop"
|
Stop ProcessState = "stop" // Process is stopped
|
||||||
Error ProcessState = "error"
|
Error ProcessState = "error" // Process is in error state
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Status represents comprehensive system and application status information.
|
||||||
|
// It includes CPU, memory, disk, network statistics, and Xray process status.
|
||||||
type Status struct {
|
type Status struct {
|
||||||
T time.Time `json:"-"`
|
T time.Time `json:"-"`
|
||||||
Cpu float64 `json:"cpu"`
|
Cpu float64 `json:"cpu"`
|
||||||
|
@ -89,10 +94,13 @@ type Status struct {
|
||||||
} `json:"appStats"`
|
} `json:"appStats"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Release represents information about a software release from GitHub.
|
||||||
type Release struct {
|
type Release struct {
|
||||||
TagName string `json:"tag_name"`
|
TagName string `json:"tag_name"` // The tag name of the release
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServerService provides business logic for server monitoring and management.
|
||||||
|
// It handles system status collection, IP detection, and application statistics.
|
||||||
type ServerService struct {
|
type ServerService struct {
|
||||||
xrayService XrayService
|
xrayService XrayService
|
||||||
inboundService InboundService
|
inboundService InboundService
|
||||||
|
@ -690,14 +698,39 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str
|
||||||
var lines []string
|
var lines []string
|
||||||
|
|
||||||
if syslog == "true" {
|
if syslog == "true" {
|
||||||
cmdArgs := []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count, "-p", level}
|
// Check if running on Windows - journalctl is not available
|
||||||
// Run the command
|
if runtime.GOOS == "windows" {
|
||||||
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
return []string{"Syslog is not supported on Windows. Please use application logs instead by unchecking the 'Syslog' option."}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and sanitize count parameter
|
||||||
|
countInt, err := strconv.Atoi(count)
|
||||||
|
if err != nil || countInt < 1 || countInt > 10000 {
|
||||||
|
return []string{"Invalid count parameter - must be a number between 1 and 10000"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate level parameter - only allow valid syslog levels
|
||||||
|
validLevels := map[string]bool{
|
||||||
|
"0": true, "emerg": true,
|
||||||
|
"1": true, "alert": true,
|
||||||
|
"2": true, "crit": true,
|
||||||
|
"3": true, "err": true,
|
||||||
|
"4": true, "warning": true,
|
||||||
|
"5": true, "notice": true,
|
||||||
|
"6": true, "info": true,
|
||||||
|
"7": true, "debug": true,
|
||||||
|
}
|
||||||
|
if !validLevels[level] {
|
||||||
|
return []string{"Invalid level parameter - must be a valid syslog level"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use hardcoded command with validated parameters
|
||||||
|
cmd := exec.Command("journalctl", "-u", "x-ui", "--no-pager", "-n", strconv.Itoa(countInt), "-p", level)
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
cmd.Stdout = &out
|
cmd.Stdout = &out
|
||||||
err := cmd.Run()
|
err = cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{"Failed to run journalctl command!"}
|
return []string{"Failed to run journalctl command! Make sure systemd is available and x-ui service is registered."}
|
||||||
}
|
}
|
||||||
lines = strings.Split(out.String(), "\n")
|
lines = strings.Split(out.String(), "\n")
|
||||||
} else {
|
} else {
|
||||||
|
@ -964,6 +997,35 @@ func (s *ServerService) ImportDB(file multipart.File) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValidGeofileName validates that the filename is safe for geofile operations.
|
||||||
|
// It checks for path traversal attempts and ensures the filename contains only safe characters.
|
||||||
|
func (s *ServerService) IsValidGeofileName(filename string) bool {
|
||||||
|
if filename == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for path traversal attempts
|
||||||
|
if strings.Contains(filename, "..") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for path separators (both forward and backward slash)
|
||||||
|
if strings.ContainsAny(filename, `/\`) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for absolute path indicators
|
||||||
|
if filepath.IsAbs(filename) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional security: only allow alphanumeric, dots, underscores, and hyphens
|
||||||
|
// This is stricter than the general filename regex
|
||||||
|
validGeofilePattern := `^[a-zA-Z0-9._-]+\.dat$`
|
||||||
|
matched, _ := regexp.MatchString(validGeofilePattern, filename)
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ServerService) UpdateGeofile(fileName string) error {
|
func (s *ServerService) UpdateGeofile(fileName string) error {
|
||||||
files := []struct {
|
files := []struct {
|
||||||
URL string
|
URL string
|
||||||
|
@ -977,6 +1039,25 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
|
||||||
{"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite_RU.dat"},
|
{"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite_RU.dat"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strict allowlist check to avoid writing uncontrolled files
|
||||||
|
if fileName != "" {
|
||||||
|
// Use the centralized validation function
|
||||||
|
if !s.IsValidGeofileName(fileName) {
|
||||||
|
return common.NewErrorf("Invalid geofile name: contains unsafe path characters: %s", fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the filename matches exactly one from our allowlist
|
||||||
|
isAllowed := false
|
||||||
|
for _, file := range files {
|
||||||
|
if fileName == file.FileName {
|
||||||
|
isAllowed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isAllowed {
|
||||||
|
return common.NewErrorf("Invalid geofile name: %s not in allowlist", fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
downloadFile := func(url, destPath string) error {
|
downloadFile := func(url, destPath string) error {
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1002,14 +1083,17 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
|
||||||
|
|
||||||
if fileName == "" {
|
if fileName == "" {
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
destPath := fmt.Sprintf("%s/%s", config.GetBinFolderPath(), file.FileName)
|
// Sanitize the filename from our allowlist as an extra precaution
|
||||||
|
destPath := filepath.Join(config.GetBinFolderPath(), filepath.Base(file.FileName))
|
||||||
|
|
||||||
if err := downloadFile(file.URL, destPath); err != nil {
|
if err := downloadFile(file.URL, destPath); err != nil {
|
||||||
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", file.FileName, err))
|
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", file.FileName, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
destPath := fmt.Sprintf("%s/%s", config.GetBinFolderPath(), fileName)
|
// Use filepath.Base to ensure we only get the filename component, no path traversal
|
||||||
|
safeName := filepath.Base(fileName)
|
||||||
|
destPath := filepath.Join(config.GetBinFolderPath(), safeName)
|
||||||
|
|
||||||
var fileURL string
|
var fileURL string
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
|
@ -1021,12 +1105,12 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
|
||||||
|
|
||||||
if fileURL == "" {
|
if fileURL == "" {
|
||||||
errorMessages = append(errorMessages, fmt.Sprintf("File '%s' not found in the list of Geofiles", fileName))
|
errorMessages = append(errorMessages, fmt.Sprintf("File '%s' not found in the list of Geofiles", fileName))
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if err := downloadFile(fileURL, destPath); err != nil {
|
if err := downloadFile(fileURL, destPath); err != nil {
|
||||||
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", fileName, err))
|
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", fileName, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err := s.RestartXrayService()
|
err := s.RestartXrayService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -33,7 +33,7 @@ var defaultValueMap = map[string]string{
|
||||||
"secret": random.Seq(32),
|
"secret": random.Seq(32),
|
||||||
"webBasePath": "/",
|
"webBasePath": "/",
|
||||||
"sessionMaxAge": "360",
|
"sessionMaxAge": "360",
|
||||||
"pageSize": "50",
|
"pageSize": "25",
|
||||||
"expireDiff": "0",
|
"expireDiff": "0",
|
||||||
"trafficDiff": "0",
|
"trafficDiff": "0",
|
||||||
"remarkModel": "-ieo",
|
"remarkModel": "-ieo",
|
||||||
|
@ -75,6 +75,8 @@ var defaultValueMap = map[string]string{
|
||||||
"externalTrafficInformURI": "",
|
"externalTrafficInformURI": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SettingService provides business logic for application settings management.
|
||||||
|
// It handles configuration storage, retrieval, and validation for all system settings.
|
||||||
type SettingService struct{}
|
type SettingService struct{}
|
||||||
|
|
||||||
func (s *SettingService) GetDefaultJsonConfig() (any, error) {
|
func (s *SettingService) GetDefaultJsonConfig() (any, error) {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
|
@ -44,6 +45,23 @@ var (
|
||||||
hostname string
|
hostname string
|
||||||
hashStorage *global.HashStorage
|
hashStorage *global.HashStorage
|
||||||
|
|
||||||
|
// Performance improvements
|
||||||
|
messageWorkerPool chan struct{} // Semaphore for limiting concurrent message processing
|
||||||
|
optimizedHTTPClient *http.Client // HTTP client with connection pooling and timeouts
|
||||||
|
|
||||||
|
// Simple cache for frequently accessed data
|
||||||
|
statusCache struct {
|
||||||
|
data *Status
|
||||||
|
timestamp time.Time
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
serverStatsCache struct {
|
||||||
|
data string
|
||||||
|
timestamp time.Time
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
// clients data to adding new client
|
// clients data to adding new client
|
||||||
receiver_inbound_ID int
|
receiver_inbound_ID int
|
||||||
client_Id string
|
client_Id string
|
||||||
|
@ -65,14 +83,18 @@ var (
|
||||||
|
|
||||||
var userStates = make(map[int64]string)
|
var userStates = make(map[int64]string)
|
||||||
|
|
||||||
|
// LoginStatus represents the result of a login attempt.
|
||||||
type LoginStatus byte
|
type LoginStatus byte
|
||||||
|
|
||||||
|
// Login status constants
|
||||||
const (
|
const (
|
||||||
LoginSuccess LoginStatus = 1
|
LoginSuccess LoginStatus = 1 // Login was successful
|
||||||
LoginFail LoginStatus = 0
|
LoginFail LoginStatus = 0 // Login failed
|
||||||
EmptyTelegramUserID = int64(0)
|
EmptyTelegramUserID = int64(0) // Default value for empty Telegram user ID
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Tgbot provides business logic for Telegram bot integration.
|
||||||
|
// It handles bot commands, user interactions, and status reporting via Telegram.
|
||||||
type Tgbot struct {
|
type Tgbot struct {
|
||||||
inboundService InboundService
|
inboundService InboundService
|
||||||
settingService SettingService
|
settingService SettingService
|
||||||
|
@ -81,18 +103,62 @@ type Tgbot struct {
|
||||||
lastStatus *Status
|
lastStatus *Status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewTgbot creates a new Tgbot instance.
|
||||||
func (t *Tgbot) NewTgbot() *Tgbot {
|
func (t *Tgbot) NewTgbot() *Tgbot {
|
||||||
return new(Tgbot)
|
return new(Tgbot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// I18nBot retrieves a localized message for the bot interface.
|
||||||
func (t *Tgbot) I18nBot(name string, params ...string) string {
|
func (t *Tgbot) I18nBot(name string, params ...string) string {
|
||||||
return locale.I18n(locale.Bot, name, params...)
|
return locale.I18n(locale.Bot, name, params...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHashStorage returns the hash storage instance for callback queries.
|
||||||
func (t *Tgbot) GetHashStorage() *global.HashStorage {
|
func (t *Tgbot) GetHashStorage() *global.HashStorage {
|
||||||
return hashStorage
|
return hashStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getCachedStatus returns cached server status if it's fresh enough (less than 5 seconds old)
|
||||||
|
func (t *Tgbot) getCachedStatus() (*Status, bool) {
|
||||||
|
statusCache.mutex.RLock()
|
||||||
|
defer statusCache.mutex.RUnlock()
|
||||||
|
|
||||||
|
if statusCache.data != nil && time.Since(statusCache.timestamp) < 5*time.Second {
|
||||||
|
return statusCache.data, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCachedStatus updates the status cache
|
||||||
|
func (t *Tgbot) setCachedStatus(status *Status) {
|
||||||
|
statusCache.mutex.Lock()
|
||||||
|
defer statusCache.mutex.Unlock()
|
||||||
|
|
||||||
|
statusCache.data = status
|
||||||
|
statusCache.timestamp = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCachedServerStats returns cached server stats if it's fresh enough (less than 10 seconds old)
|
||||||
|
func (t *Tgbot) getCachedServerStats() (string, bool) {
|
||||||
|
serverStatsCache.mutex.RLock()
|
||||||
|
defer serverStatsCache.mutex.RUnlock()
|
||||||
|
|
||||||
|
if serverStatsCache.data != "" && time.Since(serverStatsCache.timestamp) < 10*time.Second {
|
||||||
|
return serverStatsCache.data, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCachedServerStats updates the server stats cache
|
||||||
|
func (t *Tgbot) setCachedServerStats(stats string) {
|
||||||
|
serverStatsCache.mutex.Lock()
|
||||||
|
defer serverStatsCache.mutex.Unlock()
|
||||||
|
|
||||||
|
serverStatsCache.data = stats
|
||||||
|
serverStatsCache.timestamp = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initializes and starts the Telegram bot with the provided translation files.
|
||||||
func (t *Tgbot) Start(i18nFS embed.FS) error {
|
func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
// Initialize localizer
|
// Initialize localizer
|
||||||
err := locale.InitLocalizer(i18nFS, &t.settingService)
|
err := locale.InitLocalizer(i18nFS, &t.settingService)
|
||||||
|
@ -103,6 +169,20 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
// Initialize hash storage to store callback queries
|
// Initialize hash storage to store callback queries
|
||||||
hashStorage = global.NewHashStorage(20 * time.Minute)
|
hashStorage = global.NewHashStorage(20 * time.Minute)
|
||||||
|
|
||||||
|
// Initialize worker pool for concurrent message processing (max 10 concurrent handlers)
|
||||||
|
messageWorkerPool = make(chan struct{}, 10)
|
||||||
|
|
||||||
|
// Initialize optimized HTTP client with connection pooling
|
||||||
|
optimizedHTTPClient = &http.Client{
|
||||||
|
Timeout: 15 * time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
MaxIdleConnsPerHost: 10,
|
||||||
|
IdleConnTimeout: 30 * time.Second,
|
||||||
|
DisableKeepAlives: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
t.SetHostname()
|
t.SetHostname()
|
||||||
|
|
||||||
// Get Telegram bot token
|
// Get Telegram bot token
|
||||||
|
@ -173,6 +253,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewBot creates a new Telegram bot instance with optional proxy and API server settings.
|
||||||
func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
|
func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
|
||||||
if proxyUrl == "" && apiServerUrl == "" {
|
if proxyUrl == "" && apiServerUrl == "" {
|
||||||
return telego.NewBot(token)
|
return telego.NewBot(token)
|
||||||
|
@ -209,10 +290,12 @@ func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*tel
|
||||||
return telego.NewBot(token, telego.WithAPIServer(apiServerUrl))
|
return telego.NewBot(token, telego.WithAPIServer(apiServerUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsRunning checks if the Telegram bot is currently running.
|
||||||
func (t *Tgbot) IsRunning() bool {
|
func (t *Tgbot) IsRunning() bool {
|
||||||
return isRunning
|
return isRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetHostname sets the hostname for the bot.
|
||||||
func (t *Tgbot) SetHostname() {
|
func (t *Tgbot) SetHostname() {
|
||||||
host, err := os.Hostname()
|
host, err := os.Hostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -223,6 +306,7 @@ func (t *Tgbot) SetHostname() {
|
||||||
hostname = host
|
hostname = host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop stops the Telegram bot and cleans up resources.
|
||||||
func (t *Tgbot) Stop() {
|
func (t *Tgbot) Stop() {
|
||||||
if botHandler != nil {
|
if botHandler != nil {
|
||||||
botHandler.Stop()
|
botHandler.Stop()
|
||||||
|
@ -232,6 +316,7 @@ func (t *Tgbot) Stop() {
|
||||||
adminIds = nil
|
adminIds = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encodeQuery encodes the query string if it's longer than 64 characters.
|
||||||
func (t *Tgbot) encodeQuery(query string) string {
|
func (t *Tgbot) encodeQuery(query string) string {
|
||||||
// NOTE: we only need to hash for more than 64 chars
|
// NOTE: we only need to hash for more than 64 chars
|
||||||
if len(query) <= 64 {
|
if len(query) <= 64 {
|
||||||
|
@ -241,6 +326,7 @@ func (t *Tgbot) encodeQuery(query string) string {
|
||||||
return hashStorage.SaveHash(query)
|
return hashStorage.SaveHash(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decodeQuery decodes a hashed query string back to its original form.
|
||||||
func (t *Tgbot) decodeQuery(query string) (string, error) {
|
func (t *Tgbot) decodeQuery(query string) (string, error) {
|
||||||
if !hashStorage.IsMD5(query) {
|
if !hashStorage.IsMD5(query) {
|
||||||
return query, nil
|
return query, nil
|
||||||
|
@ -254,9 +340,10 @@ func (t *Tgbot) decodeQuery(query string) (string, error) {
|
||||||
return decoded, nil
|
return decoded, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnReceive starts the message receiving loop for the Telegram bot.
|
||||||
func (t *Tgbot) OnReceive() {
|
func (t *Tgbot) OnReceive() {
|
||||||
params := telego.GetUpdatesParams{
|
params := telego.GetUpdatesParams{
|
||||||
Timeout: 10,
|
Timeout: 30, // Increased timeout to reduce API calls
|
||||||
}
|
}
|
||||||
|
|
||||||
updates, _ := bot.UpdatesViaLongPolling(context.Background(), ¶ms)
|
updates, _ := bot.UpdatesViaLongPolling(context.Background(), ¶ms)
|
||||||
|
@ -270,14 +357,26 @@ func (t *Tgbot) OnReceive() {
|
||||||
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
|
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
|
||||||
|
|
||||||
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||||
|
// Use goroutine with worker pool for concurrent command processing
|
||||||
|
go func() {
|
||||||
|
messageWorkerPool <- struct{}{} // Acquire worker
|
||||||
|
defer func() { <-messageWorkerPool }() // Release worker
|
||||||
|
|
||||||
delete(userStates, message.Chat.ID)
|
delete(userStates, message.Chat.ID)
|
||||||
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
|
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
|
||||||
|
}()
|
||||||
return nil
|
return nil
|
||||||
}, th.AnyCommand())
|
}, th.AnyCommand())
|
||||||
|
|
||||||
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
|
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
|
||||||
|
// Use goroutine with worker pool for concurrent callback processing
|
||||||
|
go func() {
|
||||||
|
messageWorkerPool <- struct{}{} // Acquire worker
|
||||||
|
defer func() { <-messageWorkerPool }() // Release worker
|
||||||
|
|
||||||
delete(userStates, query.Message.GetChat().ID)
|
delete(userStates, query.Message.GetChat().ID)
|
||||||
t.answerCallback(&query, checkAdmin(query.From.ID))
|
t.answerCallback(&query, checkAdmin(query.From.ID))
|
||||||
|
}()
|
||||||
return nil
|
return nil
|
||||||
}, th.AnyCallbackQueryWithMessage())
|
}, th.AnyCallbackQueryWithMessage())
|
||||||
|
|
||||||
|
@ -430,6 +529,7 @@ func (t *Tgbot) OnReceive() {
|
||||||
botHandler.Start()
|
botHandler.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// answerCommand processes incoming command messages from Telegram users.
|
||||||
func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) {
|
func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) {
|
||||||
msg, onlyMessage := "", false
|
msg, onlyMessage := "", false
|
||||||
|
|
||||||
|
@ -505,7 +605,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to send the message based on onlyMessage flag.
|
// sendResponse sends the response message based on the onlyMessage flag.
|
||||||
func (t *Tgbot) sendResponse(chatId int64, msg string, onlyMessage, isAdmin bool) {
|
func (t *Tgbot) sendResponse(chatId int64, msg string, onlyMessage, isAdmin bool) {
|
||||||
if onlyMessage {
|
if onlyMessage {
|
||||||
t.SendMsgToTgbot(chatId, msg)
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
@ -514,6 +614,7 @@ func (t *Tgbot) sendResponse(chatId int64, msg string, onlyMessage, isAdmin bool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// randomLowerAndNum generates a random string of lowercase letters and numbers.
|
||||||
func (t *Tgbot) randomLowerAndNum(length int) string {
|
func (t *Tgbot) randomLowerAndNum(length int) string {
|
||||||
charset := "abcdefghijklmnopqrstuvwxyz0123456789"
|
charset := "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
bytes := make([]byte, length)
|
bytes := make([]byte, length)
|
||||||
|
@ -524,6 +625,7 @@ func (t *Tgbot) randomLowerAndNum(length int) string {
|
||||||
return string(bytes)
|
return string(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// randomShadowSocksPassword generates a random password for Shadowsocks.
|
||||||
func (t *Tgbot) randomShadowSocksPassword() string {
|
func (t *Tgbot) randomShadowSocksPassword() string {
|
||||||
array := make([]byte, 32)
|
array := make([]byte, 32)
|
||||||
_, err := rand.Read(array)
|
_, err := rand.Read(array)
|
||||||
|
@ -533,6 +635,7 @@ func (t *Tgbot) randomShadowSocksPassword() string {
|
||||||
return base64.StdEncoding.EncodeToString(array)
|
return base64.StdEncoding.EncodeToString(array)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// answerCallback processes callback queries from inline keyboards.
|
||||||
func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
|
func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
|
||||||
chatId := callbackQuery.Message.GetChat().ID
|
chatId := callbackQuery.Message.GetChat().ID
|
||||||
|
|
||||||
|
@ -1815,6 +1918,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildInboundClientDataMessage builds a message with client data for the given inbound and protocol.
|
||||||
func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol model.Protocol) (string, error) {
|
func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol model.Protocol) (string, error) {
|
||||||
var message string
|
var message string
|
||||||
|
|
||||||
|
@ -1864,6 +1968,7 @@ func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol mo
|
||||||
return message, nil
|
return message, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildJSONForProtocol builds a JSON string for the given protocol with client data.
|
||||||
func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) {
|
func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) {
|
||||||
var jsonString string
|
var jsonString string
|
||||||
|
|
||||||
|
@ -1942,6 +2047,7 @@ func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) {
|
||||||
return jsonString, nil
|
return jsonString, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubmitAddClient submits the client addition request to the inbound service.
|
||||||
func (t *Tgbot) SubmitAddClient() (bool, error) {
|
func (t *Tgbot) SubmitAddClient() (bool, error) {
|
||||||
|
|
||||||
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||||
|
@ -1964,6 +2070,7 @@ func (t *Tgbot) SubmitAddClient() (bool, error) {
|
||||||
return t.inboundService.AddInboundClient(newInbound)
|
return t.inboundService.AddInboundClient(newInbound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkAdmin checks if the given Telegram ID is an admin.
|
||||||
func checkAdmin(tgId int64) bool {
|
func checkAdmin(tgId int64) bool {
|
||||||
for _, adminId := range adminIds {
|
for _, adminId := range adminIds {
|
||||||
if adminId == tgId {
|
if adminId == tgId {
|
||||||
|
@ -1973,6 +2080,7 @@ func checkAdmin(tgId int64) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendAnswer sends a response message with an inline keyboard to the specified chat.
|
||||||
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
||||||
numericKeyboard := tu.InlineKeyboard(
|
numericKeyboard := tu.InlineKeyboard(
|
||||||
tu.InlineKeyboardRow(
|
tu.InlineKeyboardRow(
|
||||||
|
@ -2028,6 +2136,7 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
||||||
t.SendMsgToTgbot(chatId, msg, ReplyMarkup)
|
t.SendMsgToTgbot(chatId, msg, ReplyMarkup)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendMsgToTgbot sends a message to the Telegram bot with optional reply markup.
|
||||||
func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.ReplyMarkup) {
|
func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.ReplyMarkup) {
|
||||||
if !isRunning {
|
if !isRunning {
|
||||||
return
|
return
|
||||||
|
@ -2074,7 +2183,10 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("Error sending telegram message :", err)
|
logger.Warning("Error sending telegram message :", err)
|
||||||
}
|
}
|
||||||
time.Sleep(500 * time.Millisecond)
|
// Reduced delay to improve performance (only needed for rate limiting)
|
||||||
|
if n < len(allMessages)-1 { // Only delay between messages, not after the last one
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2143,6 +2255,7 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
|
||||||
return subURL, subJsonURL, nil
|
return subURL, subJsonURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendClientSubLinks sends the subscription links for the client to the chat.
|
||||||
func (t *Tgbot) sendClientSubLinks(chatId int64, email string) {
|
func (t *Tgbot) sendClientSubLinks(chatId int64, email string) {
|
||||||
subURL, subJsonURL, err := t.buildSubscriptionURLs(email)
|
subURL, subJsonURL, err := t.buildSubscriptionURLs(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2182,12 +2295,12 @@ func (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) {
|
||||||
// Force plain text to avoid HTML page; controller respects Accept header
|
// Force plain text to avoid HTML page; controller respects Accept header
|
||||||
req.Header.Set("Accept", "text/plain, */*;q=0.1")
|
req.Header.Set("Accept", "text/plain, */*;q=0.1")
|
||||||
|
|
||||||
// Use default client with reasonable timeout via context
|
// Use optimized client with connection pooling
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := optimizedHTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
return
|
return
|
||||||
|
@ -2297,7 +2410,7 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
if resp, err := http.DefaultClient.Do(req); err == nil {
|
if resp, err := optimizedHTTPClient.Do(req); err == nil {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
encoded, _ := t.settingService.GetSubEncrypt()
|
encoded, _ := t.settingService.GetSubEncrypt()
|
||||||
|
@ -2330,7 +2443,10 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
|
||||||
tu.FileFromBytes(png, filename),
|
tu.FileFromBytes(png, filename),
|
||||||
)
|
)
|
||||||
_, _ = bot.SendDocument(context.Background(), document)
|
_, _ = bot.SendDocument(context.Background(), document)
|
||||||
time.Sleep(200 * time.Millisecond)
|
// Reduced delay for better performance
|
||||||
|
if i < max-1 { // Only delay between documents, not after the last one
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2338,6 +2454,7 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendMsgToTgbotAdmins sends a message to all admin Telegram chats.
|
||||||
func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) {
|
func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) {
|
||||||
if len(replyMarkup) > 0 {
|
if len(replyMarkup) > 0 {
|
||||||
for _, adminId := range adminIds {
|
for _, adminId := range adminIds {
|
||||||
|
@ -2350,6 +2467,7 @@ func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMark
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendReport sends a periodic report to admin chats.
|
||||||
func (t *Tgbot) SendReport() {
|
func (t *Tgbot) SendReport() {
|
||||||
runTime, err := t.settingService.GetTgbotRuntime()
|
runTime, err := t.settingService.GetTgbotRuntime()
|
||||||
if err == nil && len(runTime) > 0 {
|
if err == nil && len(runTime) > 0 {
|
||||||
|
@ -2371,6 +2489,7 @@ func (t *Tgbot) SendReport() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendBackupToAdmins sends a database backup to admin chats.
|
||||||
func (t *Tgbot) SendBackupToAdmins() {
|
func (t *Tgbot) SendBackupToAdmins() {
|
||||||
if !t.IsRunning() {
|
if !t.IsRunning() {
|
||||||
return
|
return
|
||||||
|
@ -2380,6 +2499,7 @@ func (t *Tgbot) SendBackupToAdmins() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendExhaustedToAdmins sends notifications about exhausted clients to admins.
|
||||||
func (t *Tgbot) sendExhaustedToAdmins() {
|
func (t *Tgbot) sendExhaustedToAdmins() {
|
||||||
if !t.IsRunning() {
|
if !t.IsRunning() {
|
||||||
return
|
return
|
||||||
|
@ -2389,6 +2509,7 @@ func (t *Tgbot) sendExhaustedToAdmins() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getServerUsage retrieves and formats server usage information.
|
||||||
func (t *Tgbot) getServerUsage(chatId int64, messageID ...int) string {
|
func (t *Tgbot) getServerUsage(chatId int64, messageID ...int) string {
|
||||||
info := t.prepareServerUsageInfo()
|
info := t.prepareServerUsageInfo()
|
||||||
|
|
||||||
|
@ -2410,11 +2531,22 @@ func (t *Tgbot) sendServerUsage() string {
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareServerUsageInfo prepares the server usage information string.
|
||||||
func (t *Tgbot) prepareServerUsageInfo() string {
|
func (t *Tgbot) prepareServerUsageInfo() string {
|
||||||
|
// Check if we have cached data first
|
||||||
|
if cachedStats, found := t.getCachedServerStats(); found {
|
||||||
|
return cachedStats
|
||||||
|
}
|
||||||
|
|
||||||
info, ipv4, ipv6 := "", "", ""
|
info, ipv4, ipv6 := "", "", ""
|
||||||
|
|
||||||
// get latest status of server
|
// get latest status of server with caching
|
||||||
|
if cachedStatus, found := t.getCachedStatus(); found {
|
||||||
|
t.lastStatus = cachedStatus
|
||||||
|
} else {
|
||||||
t.lastStatus = t.serverService.GetStatus(t.lastStatus)
|
t.lastStatus = t.serverService.GetStatus(t.lastStatus)
|
||||||
|
t.setCachedStatus(t.lastStatus)
|
||||||
|
}
|
||||||
onlines := p.GetOnlineClients()
|
onlines := p.GetOnlineClients()
|
||||||
|
|
||||||
info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
|
info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
|
||||||
|
@ -2456,9 +2588,14 @@ func (t *Tgbot) prepareServerUsageInfo() string {
|
||||||
info += t.I18nBot("tgbot.messages.udpCount", "Count=="+strconv.Itoa(t.lastStatus.UdpCount))
|
info += t.I18nBot("tgbot.messages.udpCount", "Count=="+strconv.Itoa(t.lastStatus.UdpCount))
|
||||||
info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), "Upload=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), "Download=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv)))
|
info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), "Upload=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), "Download=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv)))
|
||||||
info += t.I18nBot("tgbot.messages.xrayStatus", "State=="+fmt.Sprint(t.lastStatus.Xray.State))
|
info += t.I18nBot("tgbot.messages.xrayStatus", "State=="+fmt.Sprint(t.lastStatus.Xray.State))
|
||||||
|
|
||||||
|
// Cache the complete server stats
|
||||||
|
t.setCachedServerStats(info)
|
||||||
|
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserLoginNotify sends a notification about user login attempts to admins.
|
||||||
func (t *Tgbot) UserLoginNotify(username string, password string, ip string, time string, status LoginStatus) {
|
func (t *Tgbot) UserLoginNotify(username string, password string, ip string, time string, status LoginStatus) {
|
||||||
if !t.IsRunning() {
|
if !t.IsRunning() {
|
||||||
return
|
return
|
||||||
|
@ -2490,6 +2627,7 @@ func (t *Tgbot) UserLoginNotify(username string, password string, ip string, tim
|
||||||
t.SendMsgToTgbotAdmins(msg)
|
t.SendMsgToTgbotAdmins(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getInboundUsages retrieves and formats inbound usage information.
|
||||||
func (t *Tgbot) getInboundUsages() string {
|
func (t *Tgbot) getInboundUsages() string {
|
||||||
info := ""
|
info := ""
|
||||||
// get traffic
|
// get traffic
|
||||||
|
@ -2515,6 +2653,8 @@ func (t *Tgbot) getInboundUsages() string {
|
||||||
}
|
}
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getInbounds creates an inline keyboard with all inbounds.
|
||||||
func (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) {
|
func (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) {
|
||||||
inbounds, err := t.inboundService.GetAllInbounds()
|
inbounds, err := t.inboundService.GetAllInbounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2546,8 +2686,7 @@ func (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) {
|
||||||
return keyboard, nil
|
return keyboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getInboundsFor builds an inline keyboard of inbounds where each button leads to a custom next action
|
// getInboundsFor builds an inline keyboard of inbounds for a custom next action.
|
||||||
// nextAction should be one of: get_clients_for_sub|get_clients_for_individual|get_clients_for_qr
|
|
||||||
func (t *Tgbot) getInboundsFor(nextAction string) (*telego.InlineKeyboardMarkup, error) {
|
func (t *Tgbot) getInboundsFor(nextAction string) (*telego.InlineKeyboardMarkup, error) {
|
||||||
inbounds, err := t.inboundService.GetAllInbounds()
|
inbounds, err := t.inboundService.GetAllInbounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2614,6 +2753,7 @@ func (t *Tgbot) getInboundClientsFor(inboundID int, action string) (*telego.Inli
|
||||||
return keyboard, nil
|
return keyboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getInboundsAddClient creates an inline keyboard for adding clients to inbounds.
|
||||||
func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
|
func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
|
||||||
inbounds, err := t.inboundService.GetAllInbounds()
|
inbounds, err := t.inboundService.GetAllInbounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2656,6 +2796,7 @@ func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
|
||||||
return keyboard, nil
|
return keyboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getInboundClients creates an inline keyboard with clients of a specific inbound.
|
||||||
func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) {
|
func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) {
|
||||||
inbound, err := t.inboundService.GetInbound(id)
|
inbound, err := t.inboundService.GetInbound(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2690,6 +2831,7 @@ func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error)
|
||||||
return keyboard, nil
|
return keyboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clientInfoMsg formats client information message based on traffic and flags.
|
||||||
func (t *Tgbot) clientInfoMsg(
|
func (t *Tgbot) clientInfoMsg(
|
||||||
traffic *xray.ClientTraffic,
|
traffic *xray.ClientTraffic,
|
||||||
printEnabled bool,
|
printEnabled bool,
|
||||||
|
@ -2796,6 +2938,7 @@ func (t *Tgbot) clientInfoMsg(
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getClientUsage retrieves and sends client usage information to the chat.
|
||||||
func (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) {
|
func (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) {
|
||||||
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
|
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2838,6 +2981,7 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) {
|
||||||
t.SendAnswer(chatId, output, false)
|
t.SendAnswer(chatId, output, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// searchClientIps searches and sends client IP addresses for the given email.
|
||||||
func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
|
func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
|
||||||
ips, err := t.inboundService.GetInboundClientIps(email)
|
ips, err := t.inboundService.GetInboundClientIps(email)
|
||||||
if err != nil || len(ips) == 0 {
|
if err != nil || len(ips) == 0 {
|
||||||
|
@ -2865,6 +3009,7 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clientTelegramUserInfo retrieves and sends Telegram user info for the client.
|
||||||
func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...int) {
|
func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...int) {
|
||||||
traffic, client, err := t.inboundService.GetClientByEmail(email)
|
traffic, client, err := t.inboundService.GetClientByEmail(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2917,6 +3062,7 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// searchClient searches for a client by email and sends the information.
|
||||||
func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
|
func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
|
||||||
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
|
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2962,6 +3108,7 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addClient handles the process of adding a new client to an inbound.
|
||||||
func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
|
func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
|
||||||
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -3058,6 +3205,7 @@ func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// searchInbound searches for inbounds by remark and sends the results.
|
||||||
func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
||||||
inbounds, err := t.inboundService.SearchInbounds(remark)
|
inbounds, err := t.inboundService.SearchInbounds(remark)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -3095,6 +3243,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getExhausted retrieves and sends information about exhausted clients.
|
||||||
func (t *Tgbot) getExhausted(chatId int64) {
|
func (t *Tgbot) getExhausted(chatId int64) {
|
||||||
trDiff := int64(0)
|
trDiff := int64(0)
|
||||||
exDiff := int64(0)
|
exDiff := int64(0)
|
||||||
|
@ -3191,6 +3340,7 @@ func (t *Tgbot) getExhausted(chatId int64) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// notifyExhausted sends notifications for exhausted clients.
|
||||||
func (t *Tgbot) notifyExhausted() {
|
func (t *Tgbot) notifyExhausted() {
|
||||||
trDiff := int64(0)
|
trDiff := int64(0)
|
||||||
exDiff := int64(0)
|
exDiff := int64(0)
|
||||||
|
@ -3262,6 +3412,7 @@ func (t *Tgbot) notifyExhausted() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// int64Contains checks if an int64 slice contains a specific item.
|
||||||
func int64Contains(slice []int64, item int64) bool {
|
func int64Contains(slice []int64, item int64) bool {
|
||||||
for _, s := range slice {
|
for _, s := range slice {
|
||||||
if s == item {
|
if s == item {
|
||||||
|
@ -3271,6 +3422,7 @@ func int64Contains(slice []int64, item int64) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// onlineClients retrieves and sends information about online clients.
|
||||||
func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
|
func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
|
||||||
if !p.IsRunning() {
|
if !p.IsRunning() {
|
||||||
return
|
return
|
||||||
|
@ -3305,6 +3457,7 @@ func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendBackup sends a backup of the database and configuration files.
|
||||||
func (t *Tgbot) sendBackup(chatId int64) {
|
func (t *Tgbot) sendBackup(chatId int64) {
|
||||||
output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||||
t.SendMsgToTgbot(chatId, output)
|
t.SendMsgToTgbot(chatId, output)
|
||||||
|
@ -3344,6 +3497,7 @@ func (t *Tgbot) sendBackup(chatId int64) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendBanLogs sends the ban logs to the specified chat.
|
||||||
func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
|
func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
|
||||||
if dt {
|
if dt {
|
||||||
output := t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05"))
|
output := t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||||
|
@ -3393,6 +3547,7 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendCallbackAnswerTgBot answers a callback query with a message.
|
||||||
func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
|
func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
|
||||||
params := telego.AnswerCallbackQueryParams{
|
params := telego.AnswerCallbackQueryParams{
|
||||||
CallbackQueryID: id,
|
CallbackQueryID: id,
|
||||||
|
@ -3403,6 +3558,7 @@ func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editMessageCallbackTgBot edits the reply markup of a message.
|
||||||
func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard *telego.InlineKeyboardMarkup) {
|
func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard *telego.InlineKeyboardMarkup) {
|
||||||
params := telego.EditMessageReplyMarkupParams{
|
params := telego.EditMessageReplyMarkupParams{
|
||||||
ChatID: tu.ID(chatId),
|
ChatID: tu.ID(chatId),
|
||||||
|
@ -3414,6 +3570,7 @@ func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editMessageTgBot edits the text and reply markup of a message.
|
||||||
func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlineKeyboard ...*telego.InlineKeyboardMarkup) {
|
func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlineKeyboard ...*telego.InlineKeyboardMarkup) {
|
||||||
params := telego.EditMessageTextParams{
|
params := telego.EditMessageTextParams{
|
||||||
ChatID: tu.ID(chatId),
|
ChatID: tu.ID(chatId),
|
||||||
|
@ -3429,6 +3586,7 @@ func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendMsgToTgbotDeleteAfter sends a message and deletes it after a specified delay.
|
||||||
func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSeconds int, replyMarkup ...telego.ReplyMarkup) {
|
func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSeconds int, replyMarkup ...telego.ReplyMarkup) {
|
||||||
// Determine if replyMarkup was passed; otherwise, set it to nil
|
// Determine if replyMarkup was passed; otherwise, set it to nil
|
||||||
var replyMarkupParam telego.ReplyMarkup
|
var replyMarkupParam telego.ReplyMarkup
|
||||||
|
@ -3455,6 +3613,7 @@ func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSecon
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deleteMessageTgBot deletes a message from the chat.
|
||||||
func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) {
|
func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) {
|
||||||
params := telego.DeleteMessageParams{
|
params := telego.DeleteMessageParams{
|
||||||
ChatID: tu.ID(chatId),
|
ChatID: tu.ID(chatId),
|
||||||
|
@ -3467,6 +3626,7 @@ func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isSingleWord checks if the text contains only a single word.
|
||||||
func (t *Tgbot) isSingleWord(text string) bool {
|
func (t *Tgbot) isSingleWord(text string) bool {
|
||||||
text = strings.TrimSpace(text)
|
text = strings.TrimSpace(text)
|
||||||
re := regexp.MustCompile(`\s+`)
|
re := regexp.MustCompile(`\s+`)
|
||||||
|
|
|
@ -12,10 +12,14 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UserService provides business logic for user management and authentication.
|
||||||
|
// It handles user creation, login, password management, and 2FA operations.
|
||||||
type UserService struct {
|
type UserService struct {
|
||||||
settingService SettingService
|
settingService SettingService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFirstUser retrieves the first user from the database.
|
||||||
|
// This is typically used for initial setup or when there's only one admin user.
|
||||||
func (s *UserService) GetFirstUser() (*model.User, error) {
|
func (s *UserService) GetFirstUser() (*model.User, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WarpService provides business logic for Cloudflare WARP integration.
|
||||||
|
// It manages WARP configuration and connectivity settings.
|
||||||
type WarpService struct {
|
type WarpService struct {
|
||||||
SettingService
|
SettingService
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,16 +20,20 @@ var (
|
||||||
result string
|
result string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// XrayService provides business logic for Xray process management.
|
||||||
|
// It handles starting, stopping, restarting Xray, and managing its configuration.
|
||||||
type XrayService struct {
|
type XrayService struct {
|
||||||
inboundService InboundService
|
inboundService InboundService
|
||||||
settingService SettingService
|
settingService SettingService
|
||||||
xrayAPI xray.XrayAPI
|
xrayAPI xray.XrayAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsXrayRunning checks if the Xray process is currently running.
|
||||||
func (s *XrayService) IsXrayRunning() bool {
|
func (s *XrayService) IsXrayRunning() bool {
|
||||||
return p != nil && p.IsRunning()
|
return p != nil && p.IsRunning()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetXrayErr returns the error from the Xray process, if any.
|
||||||
func (s *XrayService) GetXrayErr() error {
|
func (s *XrayService) GetXrayErr() error {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -46,6 +50,7 @@ func (s *XrayService) GetXrayErr() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetXrayResult returns the result string from the Xray process.
|
||||||
func (s *XrayService) GetXrayResult() string {
|
func (s *XrayService) GetXrayResult() string {
|
||||||
if result != "" {
|
if result != "" {
|
||||||
return result
|
return result
|
||||||
|
@ -68,6 +73,7 @@ func (s *XrayService) GetXrayResult() string {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetXrayVersion returns the version of the running Xray process.
|
||||||
func (s *XrayService) GetXrayVersion() string {
|
func (s *XrayService) GetXrayVersion() string {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
|
@ -75,10 +81,13 @@ func (s *XrayService) GetXrayVersion() string {
|
||||||
return p.GetVersion()
|
return p.GetVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveIndex removes an element at the specified index from a slice.
|
||||||
|
// Returns a new slice with the element removed.
|
||||||
func RemoveIndex(s []any, index int) []any {
|
func RemoveIndex(s []any, index int) []any {
|
||||||
return append(s[:index], s[index+1:]...)
|
return append(s[:index], s[index+1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetXrayConfig retrieves and builds the Xray configuration from settings and inbounds.
|
||||||
func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||||
templateConfig, err := s.settingService.GetXrayConfigTemplate()
|
templateConfig, err := s.settingService.GetXrayConfigTemplate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -182,6 +191,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||||
return xrayConfig, nil
|
return xrayConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetXrayTraffic fetches the current traffic statistics from the running Xray process.
|
||||||
func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
|
func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
|
||||||
if !s.IsXrayRunning() {
|
if !s.IsXrayRunning() {
|
||||||
err := errors.New("xray is not running")
|
err := errors.New("xray is not running")
|
||||||
|
@ -200,6 +210,7 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic,
|
||||||
return traffic, clientTraffic, nil
|
return traffic, clientTraffic, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RestartXray restarts the Xray process, optionally forcing a restart even if config unchanged.
|
||||||
func (s *XrayService) RestartXray(isForce bool) error {
|
func (s *XrayService) RestartXray(isForce bool) error {
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
|
@ -229,6 +240,7 @@ func (s *XrayService) RestartXray(isForce bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StopXray stops the running Xray process.
|
||||||
func (s *XrayService) StopXray() error {
|
func (s *XrayService) StopXray() error {
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
|
@ -240,15 +252,17 @@ func (s *XrayService) StopXray() error {
|
||||||
return errors.New("xray is not running")
|
return errors.New("xray is not running")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetToNeedRestart marks that Xray needs to be restarted.
|
||||||
func (s *XrayService) SetToNeedRestart() {
|
func (s *XrayService) SetToNeedRestart() {
|
||||||
isNeedXrayRestart.Store(true)
|
isNeedXrayRestart.Store(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsNeedRestartAndSetFalse checks if restart is needed and resets the flag to false.
|
||||||
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
|
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
|
||||||
return isNeedXrayRestart.CompareAndSwap(true, false)
|
return isNeedXrayRestart.CompareAndSwap(true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Xray is not running and wasn't stopped manually, i.e. crashed
|
// DidXrayCrash checks if Xray crashed by verifying it's not running and wasn't manually stopped.
|
||||||
func (s *XrayService) DidXrayCrash() bool {
|
func (s *XrayService) DidXrayCrash() bool {
|
||||||
return !s.IsXrayRunning() && !isManuallyStopped.Load()
|
return !s.IsXrayRunning() && !isManuallyStopped.Load()
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// XraySettingService provides business logic for Xray configuration management.
|
||||||
|
// It handles validation and storage of Xray template configurations.
|
||||||
type XraySettingService struct {
|
type XraySettingService struct {
|
||||||
SettingService
|
SettingService
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package session provides session management utilities for the 3x-ui web panel.
|
||||||
|
// It handles user authentication state, login sessions, and session storage using Gin sessions.
|
||||||
package session
|
package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -19,6 +21,8 @@ func init() {
|
||||||
gob.Register(model.User{})
|
gob.Register(model.User{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLoginUser stores the authenticated user in the session.
|
||||||
|
// The user object is serialized and stored for subsequent requests.
|
||||||
func SetLoginUser(c *gin.Context, user *model.User) {
|
func SetLoginUser(c *gin.Context, user *model.User) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return
|
return
|
||||||
|
@ -27,6 +31,8 @@ func SetLoginUser(c *gin.Context, user *model.User) {
|
||||||
s.Set(loginUserKey, *user)
|
s.Set(loginUserKey, *user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMaxAge configures the session cookie maximum age in seconds.
|
||||||
|
// This controls how long the session remains valid before requiring re-authentication.
|
||||||
func SetMaxAge(c *gin.Context, maxAge int) {
|
func SetMaxAge(c *gin.Context, maxAge int) {
|
||||||
s := sessions.Default(c)
|
s := sessions.Default(c)
|
||||||
s.Options(sessions.Options{
|
s.Options(sessions.Options{
|
||||||
|
@ -37,6 +43,8 @@ func SetMaxAge(c *gin.Context, maxAge int) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLoginUser retrieves the authenticated user from the session.
|
||||||
|
// Returns nil if no user is logged in or if the session data is invalid.
|
||||||
func GetLoginUser(c *gin.Context) *model.User {
|
func GetLoginUser(c *gin.Context) *model.User {
|
||||||
s := sessions.Default(c)
|
s := sessions.Default(c)
|
||||||
obj := s.Get(loginUserKey)
|
obj := s.Get(loginUserKey)
|
||||||
|
@ -52,10 +60,14 @@ func GetLoginUser(c *gin.Context) *model.User {
|
||||||
return &user
|
return &user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsLogin checks if a user is currently authenticated in the session.
|
||||||
|
// Returns true if a valid user session exists, false otherwise.
|
||||||
func IsLogin(c *gin.Context) bool {
|
func IsLogin(c *gin.Context) bool {
|
||||||
return GetLoginUser(c) != nil
|
return GetLoginUser(c) != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearSession removes all session data and invalidates the session.
|
||||||
|
// This effectively logs out the user and clears any stored session information.
|
||||||
func ClearSession(c *gin.Context) {
|
func ClearSession(c *gin.Context) {
|
||||||
s := sessions.Default(c)
|
s := sessions.Default(c)
|
||||||
s.Clear()
|
s.Clear()
|
||||||
|
|
20
web/web.go
20
web/web.go
|
@ -1,3 +1,5 @@
|
||||||
|
// Package web provides the main web server implementation for the 3x-ui panel,
|
||||||
|
// including HTTP/HTTPS serving, routing, templates, and background job scheduling.
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -78,15 +80,17 @@ func (f *wrapAssetsFileInfo) ModTime() time.Time {
|
||||||
return startTime
|
return startTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose embedded resources for reuse by other servers (e.g., sub server)
|
// EmbeddedHTML returns the embedded HTML templates filesystem for reuse by other servers.
|
||||||
func EmbeddedHTML() embed.FS {
|
func EmbeddedHTML() embed.FS {
|
||||||
return htmlFS
|
return htmlFS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EmbeddedAssets returns the embedded assets filesystem for reuse by other servers.
|
||||||
func EmbeddedAssets() embed.FS {
|
func EmbeddedAssets() embed.FS {
|
||||||
return assetsFS
|
return assetsFS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server represents the main web server for the 3x-ui panel with controllers, services, and scheduled jobs.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
|
@ -106,6 +110,7 @@ type Server struct {
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewServer creates a new web server instance with a cancellable context.
|
||||||
func NewServer() *Server {
|
func NewServer() *Server {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
return &Server{
|
return &Server{
|
||||||
|
@ -114,6 +119,8 @@ func NewServer() *Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getHtmlFiles walks the local `web/html` directory and returns a list of
|
||||||
|
// template file paths. Used only in debug/development mode.
|
||||||
func (s *Server) getHtmlFiles() ([]string, error) {
|
func (s *Server) getHtmlFiles() ([]string, error) {
|
||||||
files := make([]string, 0)
|
files := make([]string, 0)
|
||||||
dir, _ := os.Getwd()
|
dir, _ := os.Getwd()
|
||||||
|
@ -133,6 +140,9 @@ func (s *Server) getHtmlFiles() ([]string, error) {
|
||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getHtmlTemplate parses embedded HTML templates from the bundled `htmlFS`
|
||||||
|
// using the provided template function map and returns the resulting
|
||||||
|
// template set for production usage.
|
||||||
func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template, error) {
|
func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template, error) {
|
||||||
t := template.New("").Funcs(funcMap)
|
t := template.New("").Funcs(funcMap)
|
||||||
err := fs.WalkDir(htmlFS, "html", func(path string, d fs.DirEntry, err error) error {
|
err := fs.WalkDir(htmlFS, "html", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
@ -156,6 +166,8 @@ func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template,
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initRouter initializes Gin, registers middleware, templates, static
|
||||||
|
// assets, controllers and returns the configured engine.
|
||||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
if config.IsDebug() {
|
if config.IsDebug() {
|
||||||
gin.SetMode(gin.DebugMode)
|
gin.SetMode(gin.DebugMode)
|
||||||
|
@ -259,6 +271,8 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
return engine, nil
|
return engine, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// startTask schedules background jobs (Xray checks, traffic jobs, cron
|
||||||
|
// jobs) which the panel relies on for periodic maintenance and monitoring.
|
||||||
func (s *Server) startTask() {
|
func (s *Server) startTask() {
|
||||||
err := s.xrayService.RestartXray(true)
|
err := s.xrayService.RestartXray(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -326,6 +340,7 @@ func (s *Server) startTask() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start initializes and starts the web server with configured settings, routes, and background jobs.
|
||||||
func (s *Server) Start() (err error) {
|
func (s *Server) Start() (err error) {
|
||||||
// This is an anonymous function, no function name
|
// This is an anonymous function, no function name
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -404,6 +419,7 @@ func (s *Server) Start() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop gracefully shuts down the web server, stops Xray, cron jobs, and Telegram bot.
|
||||||
func (s *Server) Stop() error {
|
func (s *Server) Stop() error {
|
||||||
s.cancel()
|
s.cancel()
|
||||||
s.xrayService.StopXray()
|
s.xrayService.StopXray()
|
||||||
|
@ -424,10 +440,12 @@ func (s *Server) Stop() error {
|
||||||
return common.Combine(err1, err2)
|
return common.Combine(err1, err2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCtx returns the server's context for cancellation and deadline management.
|
||||||
func (s *Server) GetCtx() context.Context {
|
func (s *Server) GetCtx() context.Context {
|
||||||
return s.ctx
|
return s.ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCron returns the server's cron scheduler instance.
|
||||||
func (s *Server) GetCron() *cron.Cron {
|
func (s *Server) GetCron() *cron.Cron {
|
||||||
return s.cron
|
return s.cron
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
BIN
windows_files/SSL/Win64OpenSSL_Light-3_5_3.exe
Normal file
BIN
windows_files/SSL/Win64OpenSSL_Light-3_5_3.exe
Normal file
Binary file not shown.
14
xray/api.go
14
xray/api.go
|
@ -1,3 +1,6 @@
|
||||||
|
// Package xray provides integration with the Xray proxy core.
|
||||||
|
// It includes API client functionality, configuration management, traffic monitoring,
|
||||||
|
// and process control for Xray instances.
|
||||||
package xray
|
package xray
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -25,6 +28,7 @@ import (
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// XrayAPI is a gRPC client for managing Xray core configuration, inbounds, outbounds, and statistics.
|
||||||
type XrayAPI struct {
|
type XrayAPI struct {
|
||||||
HandlerServiceClient *command.HandlerServiceClient
|
HandlerServiceClient *command.HandlerServiceClient
|
||||||
StatsServiceClient *statsService.StatsServiceClient
|
StatsServiceClient *statsService.StatsServiceClient
|
||||||
|
@ -32,6 +36,7 @@ type XrayAPI struct {
|
||||||
isConnected bool
|
isConnected bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init connects to the Xray API server and initializes handler and stats service clients.
|
||||||
func (x *XrayAPI) Init(apiPort int) error {
|
func (x *XrayAPI) Init(apiPort int) error {
|
||||||
if apiPort <= 0 || apiPort > math.MaxUint16 {
|
if apiPort <= 0 || apiPort > math.MaxUint16 {
|
||||||
return fmt.Errorf("invalid Xray API port: %d", apiPort)
|
return fmt.Errorf("invalid Xray API port: %d", apiPort)
|
||||||
|
@ -55,6 +60,7 @@ func (x *XrayAPI) Init(apiPort int) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close closes the gRPC connection and resets the XrayAPI client state.
|
||||||
func (x *XrayAPI) Close() {
|
func (x *XrayAPI) Close() {
|
||||||
if x.grpcClient != nil {
|
if x.grpcClient != nil {
|
||||||
x.grpcClient.Close()
|
x.grpcClient.Close()
|
||||||
|
@ -64,6 +70,7 @@ func (x *XrayAPI) Close() {
|
||||||
x.isConnected = false
|
x.isConnected = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddInbound adds a new inbound configuration to the Xray core via gRPC.
|
||||||
func (x *XrayAPI) AddInbound(inbound []byte) error {
|
func (x *XrayAPI) AddInbound(inbound []byte) error {
|
||||||
client := *x.HandlerServiceClient
|
client := *x.HandlerServiceClient
|
||||||
|
|
||||||
|
@ -85,6 +92,7 @@ func (x *XrayAPI) AddInbound(inbound []byte) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DelInbound removes an inbound configuration from the Xray core by tag.
|
||||||
func (x *XrayAPI) DelInbound(tag string) error {
|
func (x *XrayAPI) DelInbound(tag string) error {
|
||||||
client := *x.HandlerServiceClient
|
client := *x.HandlerServiceClient
|
||||||
_, err := client.RemoveInbound(context.Background(), &command.RemoveInboundRequest{
|
_, err := client.RemoveInbound(context.Background(), &command.RemoveInboundRequest{
|
||||||
|
@ -93,6 +101,7 @@ func (x *XrayAPI) DelInbound(tag string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddUser adds a user to an inbound in the Xray core using the specified protocol and user data.
|
||||||
func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]any) error {
|
func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]any) error {
|
||||||
var account *serial.TypedMessage
|
var account *serial.TypedMessage
|
||||||
switch Protocol {
|
switch Protocol {
|
||||||
|
@ -153,6 +162,7 @@ func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]an
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveUser removes a user from an inbound in the Xray core by email.
|
||||||
func (x *XrayAPI) RemoveUser(inboundTag, email string) error {
|
func (x *XrayAPI) RemoveUser(inboundTag, email string) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -171,6 +181,7 @@ func (x *XrayAPI) RemoveUser(inboundTag, email string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTraffic queries traffic statistics from the Xray core, optionally resetting counters.
|
||||||
func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
|
func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
|
||||||
if x.grpcClient == nil {
|
if x.grpcClient == nil {
|
||||||
return nil, nil, common.NewError("xray api is not initialized")
|
return nil, nil, common.NewError("xray api is not initialized")
|
||||||
|
@ -205,6 +216,7 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
|
||||||
return mapToSlice(tagTrafficMap), mapToSlice(emailTrafficMap), nil
|
return mapToSlice(tagTrafficMap), mapToSlice(emailTrafficMap), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processTraffic aggregates a traffic stat into trafficMap using regex matches and value.
|
||||||
func processTraffic(matches []string, value int64, trafficMap map[string]*Traffic) {
|
func processTraffic(matches []string, value int64, trafficMap map[string]*Traffic) {
|
||||||
isInbound := matches[1] == "inbound"
|
isInbound := matches[1] == "inbound"
|
||||||
tag := matches[2]
|
tag := matches[2]
|
||||||
|
@ -231,6 +243,7 @@ func processTraffic(matches []string, value int64, trafficMap map[string]*Traffi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processClientTraffic updates clientTrafficMap with upload/download values for a client email.
|
||||||
func processClientTraffic(matches []string, value int64, clientTrafficMap map[string]*ClientTraffic) {
|
func processClientTraffic(matches []string, value int64, clientTrafficMap map[string]*ClientTraffic) {
|
||||||
email := matches[1]
|
email := matches[1]
|
||||||
isDown := matches[2] == "downlink"
|
isDown := matches[2] == "downlink"
|
||||||
|
@ -248,6 +261,7 @@ func processClientTraffic(matches []string, value int64, clientTrafficMap map[st
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mapToSlice converts a map of pointers to a slice of pointers.
|
||||||
func mapToSlice[T any](m map[string]*T) []*T {
|
func mapToSlice[T any](m map[string]*T) []*T {
|
||||||
result := make([]*T, 0, len(m))
|
result := make([]*T, 0, len(m))
|
||||||
for _, v := range m {
|
for _, v := range m {
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
package xray
|
package xray
|
||||||
|
|
||||||
|
// ClientTraffic represents traffic statistics and limits for a specific client.
|
||||||
|
// It tracks upload/download usage, expiry times, and online status for inbound clients.
|
||||||
type ClientTraffic struct {
|
type ClientTraffic struct {
|
||||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
InboundId int `json:"inboundId" form:"inboundId"`
|
InboundId int `json:"inboundId" form:"inboundId"`
|
||||||
Enable bool `json:"enable" form:"enable"`
|
Enable bool `json:"enable" form:"enable"`
|
||||||
Email string `json:"email" form:"email" gorm:"unique"`
|
Email string `json:"email" form:"email" gorm:"unique"`
|
||||||
|
UUID string `json:"uuid" form:"uuid" gorm:"-"`
|
||||||
SubId string `json:"subId" form:"subId" gorm:"-"`
|
SubId string `json:"subId" form:"subId" gorm:"-"`
|
||||||
Up int64 `json:"up" form:"up"`
|
Up int64 `json:"up" form:"up"`
|
||||||
Down int64 `json:"down" form:"down"`
|
Down int64 `json:"down" form:"down"`
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config represents the complete Xray configuration structure.
|
||||||
|
// It contains all sections of an Xray config file including inbounds, outbounds, routing, etc.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
LogConfig json_util.RawMessage `json:"log"`
|
LogConfig json_util.RawMessage `json:"log"`
|
||||||
RouterConfig json_util.RawMessage `json:"routing"`
|
RouterConfig json_util.RawMessage `json:"routing"`
|
||||||
|
@ -23,6 +25,7 @@ type Config struct {
|
||||||
Metrics json_util.RawMessage `json:"metrics"`
|
Metrics json_util.RawMessage `json:"metrics"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Equals compares two Config instances for deep equality.
|
||||||
func (c *Config) Equals(other *Config) bool {
|
func (c *Config) Equals(other *Config) bool {
|
||||||
if len(c.InboundConfigs) != len(other.InboundConfigs) {
|
if len(c.InboundConfigs) != len(other.InboundConfigs) {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// InboundConfig represents an Xray inbound configuration.
|
||||||
|
// It defines how Xray accepts incoming connections including protocol, port, and settings.
|
||||||
type InboundConfig struct {
|
type InboundConfig struct {
|
||||||
Listen json_util.RawMessage `json:"listen"` // listen cannot be an empty string
|
Listen json_util.RawMessage `json:"listen"` // listen cannot be an empty string
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
|
@ -16,6 +18,7 @@ type InboundConfig struct {
|
||||||
Sniffing json_util.RawMessage `json:"sniffing"`
|
Sniffing json_util.RawMessage `json:"sniffing"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Equals compares two InboundConfig instances for deep equality.
|
||||||
func (c *InboundConfig) Equals(other *InboundConfig) bool {
|
func (c *InboundConfig) Equals(other *InboundConfig) bool {
|
||||||
if !bytes.Equal(c.Listen, other.Listen) {
|
if !bytes.Equal(c.Listen, other.Listen) {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -8,14 +8,17 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NewLogWriter returns a new LogWriter for processing Xray log output.
|
||||||
func NewLogWriter() *LogWriter {
|
func NewLogWriter() *LogWriter {
|
||||||
return &LogWriter{}
|
return &LogWriter{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogWriter processes and filters log output from the Xray process, handling crash detection and message filtering.
|
||||||
type LogWriter struct {
|
type LogWriter struct {
|
||||||
lastLine string
|
lastLine string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write processes and filters log output from the Xray process, handling crash detection and message filtering.
|
||||||
func (lw *LogWriter) Write(m []byte) (n int, err error) {
|
func (lw *LogWriter) Write(m []byte) (n int, err error) {
|
||||||
crashRegex := regexp.MustCompile(`(?i)(panic|exception|stack trace|fatal error)`)
|
crashRegex := regexp.MustCompile(`(?i)(panic|exception|stack trace|fatal error)`)
|
||||||
|
|
||||||
|
|
|
@ -18,46 +18,57 @@ import (
|
||||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetBinaryName returns the Xray binary filename for the current OS and architecture.
|
||||||
func GetBinaryName() string {
|
func GetBinaryName() string {
|
||||||
return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH)
|
return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBinaryPath returns the full path to the Xray binary executable.
|
||||||
func GetBinaryPath() string {
|
func GetBinaryPath() string {
|
||||||
return config.GetBinFolderPath() + "/" + GetBinaryName()
|
return config.GetBinFolderPath() + "/" + GetBinaryName()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetConfigPath returns the path to the Xray configuration file in the binary folder.
|
||||||
func GetConfigPath() string {
|
func GetConfigPath() string {
|
||||||
return config.GetBinFolderPath() + "/config.json"
|
return config.GetBinFolderPath() + "/config.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetGeositePath returns the path to the geosite data file used by Xray.
|
||||||
func GetGeositePath() string {
|
func GetGeositePath() string {
|
||||||
return config.GetBinFolderPath() + "/geosite.dat"
|
return config.GetBinFolderPath() + "/geosite.dat"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetGeoipPath returns the path to the geoip data file used by Xray.
|
||||||
func GetGeoipPath() string {
|
func GetGeoipPath() string {
|
||||||
return config.GetBinFolderPath() + "/geoip.dat"
|
return config.GetBinFolderPath() + "/geoip.dat"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIPLimitLogPath returns the path to the IP limit log file.
|
||||||
func GetIPLimitLogPath() string {
|
func GetIPLimitLogPath() string {
|
||||||
return config.GetLogFolder() + "/3xipl.log"
|
return config.GetLogFolder() + "/3xipl.log"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIPLimitBannedLogPath returns the path to the banned IP log file.
|
||||||
func GetIPLimitBannedLogPath() string {
|
func GetIPLimitBannedLogPath() string {
|
||||||
return config.GetLogFolder() + "/3xipl-banned.log"
|
return config.GetLogFolder() + "/3xipl-banned.log"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIPLimitBannedPrevLogPath returns the path to the previous banned IP log file.
|
||||||
func GetIPLimitBannedPrevLogPath() string {
|
func GetIPLimitBannedPrevLogPath() string {
|
||||||
return config.GetLogFolder() + "/3xipl-banned.prev.log"
|
return config.GetLogFolder() + "/3xipl-banned.prev.log"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAccessPersistentLogPath returns the path to the persistent access log file.
|
||||||
func GetAccessPersistentLogPath() string {
|
func GetAccessPersistentLogPath() string {
|
||||||
return config.GetLogFolder() + "/3xipl-ap.log"
|
return config.GetLogFolder() + "/3xipl-ap.log"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAccessPersistentPrevLogPath returns the path to the previous persistent access log file.
|
||||||
func GetAccessPersistentPrevLogPath() string {
|
func GetAccessPersistentPrevLogPath() string {
|
||||||
return config.GetLogFolder() + "/3xipl-ap.prev.log"
|
return config.GetLogFolder() + "/3xipl-ap.prev.log"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAccessLogPath reads the Xray config and returns the access log file path.
|
||||||
func GetAccessLogPath() (string, error) {
|
func GetAccessLogPath() (string, error) {
|
||||||
config, err := os.ReadFile(GetConfigPath())
|
config, err := os.ReadFile(GetConfigPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -82,14 +93,17 @@ func GetAccessLogPath() (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stopProcess calls Stop on the given Process instance.
|
||||||
func stopProcess(p *Process) {
|
func stopProcess(p *Process) {
|
||||||
p.Stop()
|
p.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process wraps an Xray process instance and provides management methods.
|
||||||
type Process struct {
|
type Process struct {
|
||||||
*process
|
*process
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewProcess creates a new Xray process and sets up cleanup on garbage collection.
|
||||||
func NewProcess(xrayConfig *Config) *Process {
|
func NewProcess(xrayConfig *Config) *Process {
|
||||||
p := &Process{newProcess(xrayConfig)}
|
p := &Process{newProcess(xrayConfig)}
|
||||||
runtime.SetFinalizer(p, stopProcess)
|
runtime.SetFinalizer(p, stopProcess)
|
||||||
|
@ -110,6 +124,7 @@ type process struct {
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newProcess creates a new internal process struct for Xray.
|
||||||
func newProcess(config *Config) *process {
|
func newProcess(config *Config) *process {
|
||||||
return &process{
|
return &process{
|
||||||
version: "Unknown",
|
version: "Unknown",
|
||||||
|
@ -119,6 +134,7 @@ func newProcess(config *Config) *process {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsRunning returns true if the Xray process is currently running.
|
||||||
func (p *process) IsRunning() bool {
|
func (p *process) IsRunning() bool {
|
||||||
if p.cmd == nil || p.cmd.Process == nil {
|
if p.cmd == nil || p.cmd.Process == nil {
|
||||||
return false
|
return false
|
||||||
|
@ -129,10 +145,12 @@ func (p *process) IsRunning() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetErr returns the last error encountered by the Xray process.
|
||||||
func (p *process) GetErr() error {
|
func (p *process) GetErr() error {
|
||||||
return p.exitErr
|
return p.exitErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetResult returns the last log line or error from the Xray process.
|
||||||
func (p *process) GetResult() string {
|
func (p *process) GetResult() string {
|
||||||
if len(p.logWriter.lastLine) == 0 && p.exitErr != nil {
|
if len(p.logWriter.lastLine) == 0 && p.exitErr != nil {
|
||||||
return p.exitErr.Error()
|
return p.exitErr.Error()
|
||||||
|
@ -140,30 +158,37 @@ func (p *process) GetResult() string {
|
||||||
return p.logWriter.lastLine
|
return p.logWriter.lastLine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetVersion returns the version string of the Xray process.
|
||||||
func (p *process) GetVersion() string {
|
func (p *process) GetVersion() string {
|
||||||
return p.version
|
return p.version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAPIPort returns the API port used by the Xray process.
|
||||||
func (p *Process) GetAPIPort() int {
|
func (p *Process) GetAPIPort() int {
|
||||||
return p.apiPort
|
return p.apiPort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetConfig returns the configuration used by the Xray process.
|
||||||
func (p *Process) GetConfig() *Config {
|
func (p *Process) GetConfig() *Config {
|
||||||
return p.config
|
return p.config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetOnlineClients returns the list of online clients for the Xray process.
|
||||||
func (p *Process) GetOnlineClients() []string {
|
func (p *Process) GetOnlineClients() []string {
|
||||||
return p.onlineClients
|
return p.onlineClients
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetOnlineClients sets the list of online clients for the Xray process.
|
||||||
func (p *Process) SetOnlineClients(users []string) {
|
func (p *Process) SetOnlineClients(users []string) {
|
||||||
p.onlineClients = users
|
p.onlineClients = users
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUptime returns the uptime of the Xray process in seconds.
|
||||||
func (p *Process) GetUptime() uint64 {
|
func (p *Process) GetUptime() uint64 {
|
||||||
return uint64(time.Since(p.startTime).Seconds())
|
return uint64(time.Since(p.startTime).Seconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// refreshAPIPort updates the API port from the inbound configs.
|
||||||
func (p *process) refreshAPIPort() {
|
func (p *process) refreshAPIPort() {
|
||||||
for _, inbound := range p.config.InboundConfigs {
|
for _, inbound := range p.config.InboundConfigs {
|
||||||
if inbound.Tag == "api" {
|
if inbound.Tag == "api" {
|
||||||
|
@ -173,6 +198,7 @@ func (p *process) refreshAPIPort() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// refreshVersion updates the version string by running the Xray binary with -version.
|
||||||
func (p *process) refreshVersion() {
|
func (p *process) refreshVersion() {
|
||||||
cmd := exec.Command(GetBinaryPath(), "-version")
|
cmd := exec.Command(GetBinaryPath(), "-version")
|
||||||
data, err := cmd.Output()
|
data, err := cmd.Output()
|
||||||
|
@ -188,6 +214,7 @@ func (p *process) refreshVersion() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start launches the Xray process with the current configuration.
|
||||||
func (p *process) Start() (err error) {
|
func (p *process) Start() (err error) {
|
||||||
if p.IsRunning() {
|
if p.IsRunning() {
|
||||||
return errors.New("xray is already running")
|
return errors.New("xray is already running")
|
||||||
|
@ -245,6 +272,7 @@ func (p *process) Start() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop terminates the running Xray process.
|
||||||
func (p *process) Stop() error {
|
func (p *process) Stop() error {
|
||||||
if !p.IsRunning() {
|
if !p.IsRunning() {
|
||||||
return errors.New("xray is not running")
|
return errors.New("xray is not running")
|
||||||
|
@ -257,6 +285,7 @@ func (p *process) Stop() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeCrashReport writes a crash report to the binary folder with a timestamped filename.
|
||||||
func writeCrashReport(m []byte) error {
|
func writeCrashReport(m []byte) error {
|
||||||
crashReportPath := config.GetBinFolderPath() + "/core_crash_" + time.Now().Format("20060102_150405") + ".log"
|
crashReportPath := config.GetBinFolderPath() + "/core_crash_" + time.Now().Format("20060102_150405") + ".log"
|
||||||
return os.WriteFile(crashReportPath, m, os.ModePerm)
|
return os.WriteFile(crashReportPath, m, os.ModePerm)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package xray
|
package xray
|
||||||
|
|
||||||
|
// Traffic represents network traffic statistics for Xray connections.
|
||||||
|
// It tracks upload and download bytes for inbound or outbound traffic.
|
||||||
type Traffic struct {
|
type Traffic struct {
|
||||||
IsInbound bool
|
IsInbound bool
|
||||||
IsOutbound bool
|
IsOutbound bool
|
||||||
|
|
Loading…
Reference in a new issue