mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-07 13:44:24 +00:00
- Add splitTemplate() to split at proxies:/proxy-groups: markers (like mihomo-gen) - Store clash_template.yaml and servers.yaml as files alongside x-ui.json - Add Clash/Servers editors in Xray advanced config page - Support multi-server proxy generation (each server × each client) - Remove inline template editor from Clash settings panel - Bump version to v1.7.2.1
500 lines
12 KiB
Go
500 lines
12 KiB
Go
// Package config provides configuration management utilities for the 3x-ui panel,
|
|
// including version information, logging levels, database paths, and environment variable handling.
|
|
package config
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
//go:embed version
|
|
var version string
|
|
|
|
//go:embed name
|
|
var name string
|
|
|
|
// LogLevel represents the logging level for the application.
|
|
type LogLevel string
|
|
|
|
// Logging level constants
|
|
const (
|
|
Debug LogLevel = "debug"
|
|
Info LogLevel = "info"
|
|
Notice LogLevel = "notice"
|
|
Warning LogLevel = "warning"
|
|
Error LogLevel = "error"
|
|
)
|
|
|
|
type NodeRole string
|
|
|
|
const (
|
|
NodeRoleMaster NodeRole = "master"
|
|
NodeRoleWorker NodeRole = "worker"
|
|
)
|
|
|
|
type NodeConfig struct {
|
|
Role NodeRole
|
|
NodeID string
|
|
SyncIntervalSeconds int
|
|
TrafficFlushSeconds int
|
|
}
|
|
|
|
// GetVersion returns the version string of the 3x-ui application.
|
|
func GetVersion() string {
|
|
return strings.TrimSpace(version)
|
|
}
|
|
|
|
// GetName returns the name of the 3x-ui application.
|
|
func GetName() string {
|
|
return strings.TrimSpace(name)
|
|
}
|
|
|
|
// GetLogLevel returns the current logging level based on environment variables or defaults to Info.
|
|
func GetLogLevel() LogLevel {
|
|
if IsDebug() {
|
|
return Debug
|
|
}
|
|
logLevel := os.Getenv("XUI_LOG_LEVEL")
|
|
if logLevel == "" {
|
|
return Info
|
|
}
|
|
return LogLevel(logLevel)
|
|
}
|
|
|
|
// IsDebug returns true if debug mode is enabled via the XUI_DEBUG environment variable.
|
|
func IsDebug() bool {
|
|
return os.Getenv("XUI_DEBUG") == "true"
|
|
}
|
|
|
|
// GetBinFolderPath returns the path to the binary folder, defaulting to "bin" if not set via XUI_BIN_FOLDER.
|
|
func GetBinFolderPath() string {
|
|
binFolderPath := os.Getenv("XUI_BIN_FOLDER")
|
|
if binFolderPath == "" {
|
|
binFolderPath = "bin"
|
|
}
|
|
return binFolderPath
|
|
}
|
|
|
|
func getBaseDir() string {
|
|
exePath, err := os.Executable()
|
|
if err != nil {
|
|
return "."
|
|
}
|
|
exeDir := filepath.Dir(exePath)
|
|
exeDirLower := strings.ToLower(filepath.ToSlash(exeDir))
|
|
if strings.Contains(exeDirLower, "/appdata/local/temp/") || strings.Contains(exeDirLower, "/go-build") {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return "."
|
|
}
|
|
return wd
|
|
}
|
|
return exeDir
|
|
}
|
|
|
|
// GetDBFolderPath returns the path to the database folder based on environment variables or platform defaults.
|
|
func GetDBFolderPath() string {
|
|
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
|
|
if dbFolderPath != "" {
|
|
return dbFolderPath
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
return getBaseDir()
|
|
}
|
|
return "/etc/x-ui"
|
|
}
|
|
|
|
// GetDBPath returns the full path to the database file.
|
|
func GetDBPath() string {
|
|
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
|
}
|
|
|
|
// GetSettingPath returns the full path to the panel settings JSON file.
|
|
func GetSettingPath() string {
|
|
return fmt.Sprintf("%s/%s.json", GetDBFolderPath(), GetName())
|
|
}
|
|
|
|
func GetSharedCachePath() string {
|
|
return filepath.Join(GetDBFolderPath(), "shared-cache.json")
|
|
}
|
|
|
|
func GetTrafficPendingPath() string {
|
|
return filepath.Join(GetDBFolderPath(), "traffic-pending.json")
|
|
}
|
|
|
|
// GetClashTemplatePath returns the path to the clash_template.yaml file.
|
|
func GetClashTemplatePath() string {
|
|
return filepath.Join(GetDBFolderPath(), "clash_template.yaml")
|
|
}
|
|
|
|
// GetServersPath returns the path to the servers.yaml file.
|
|
func GetServersPath() string {
|
|
return filepath.Join(GetDBFolderPath(), "servers.yaml")
|
|
}
|
|
|
|
// ReadClashTemplate reads the clash template from disk.
|
|
// Returns the default mihomo template if the file does not exist.
|
|
func ReadClashTemplate() (string, error) {
|
|
data, err := os.ReadFile(GetClashTemplatePath())
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return defaultClashTemplate, nil
|
|
}
|
|
return "", err
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
// SaveClashTemplate writes the clash template to disk.
|
|
func SaveClashTemplate(content string) error {
|
|
path := GetClashTemplatePath()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, []byte(content), 0644)
|
|
}
|
|
|
|
// ReadServers reads the servers config from disk.
|
|
// Returns a default empty servers config if the file does not exist.
|
|
func ReadServers() (string, error) {
|
|
data, err := os.ReadFile(GetServersPath())
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return defaultServers, nil
|
|
}
|
|
return "", err
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
// SaveServers writes the servers config to disk.
|
|
func SaveServers(content string) error {
|
|
path := GetServersPath()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, []byte(content), 0644)
|
|
}
|
|
|
|
const defaultServers = `servers: []
|
|
`
|
|
|
|
const defaultClashTemplate = `port: 7890
|
|
socks-port: 7891
|
|
allow-lan: false
|
|
mode: rule
|
|
log-level: info
|
|
proxies:
|
|
proxy-groups:
|
|
- name: Proxy
|
|
type: select
|
|
proxies:
|
|
- DIRECT
|
|
dns:
|
|
enable: true
|
|
enhanced-mode: fake-ip
|
|
nameserver:
|
|
- 8.8.8.8
|
|
- 1.1.1.1
|
|
rules:
|
|
- GEOIP,LAN,DIRECT
|
|
- MATCH,Proxy
|
|
`
|
|
|
|
// GetLogFolder returns the path to the log folder based on environment variables or platform defaults.
|
|
func GetLogFolder() string {
|
|
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
|
|
if logFolderPath != "" {
|
|
return logFolderPath
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
return filepath.Join(".", "log")
|
|
}
|
|
return "/var/log/x-ui"
|
|
}
|
|
|
|
var settingGroupAliases = map[string][]string{
|
|
"dbType": {"databaseConnection", "other"},
|
|
"dbHost": {"databaseConnection", "other"},
|
|
"dbPort": {"databaseConnection", "other"},
|
|
"dbUser": {"databaseConnection", "other"},
|
|
"dbPassword": {"databaseConnection", "other"},
|
|
"dbName": {"databaseConnection", "other"},
|
|
"nodeRole": {"node", "other"},
|
|
"nodeId": {"node", "other"},
|
|
"syncInterval": {"node", "other"},
|
|
"trafficFlushInterval": {"node", "other"},
|
|
}
|
|
|
|
func readGroupedString(settings map[string]any, key string) string {
|
|
if groups, ok := settingGroupAliases[key]; ok {
|
|
for _, groupName := range groups {
|
|
if group, ok := settings[groupName].(map[string]any); ok {
|
|
if value, ok := group[key].(string); ok && value != "" {
|
|
return value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if value, ok := settings[key].(string); ok && value != "" {
|
|
return value
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func readGroupedInt(settings map[string]any, key string, fallback int) int {
|
|
readInt := func(value any) (int, bool) {
|
|
switch v := value.(type) {
|
|
case float64:
|
|
return int(v), true
|
|
case int:
|
|
return v, true
|
|
case string:
|
|
i, err := strconv.Atoi(v)
|
|
if err == nil {
|
|
return i, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
if groups, ok := settingGroupAliases[key]; ok {
|
|
for _, groupName := range groups {
|
|
if group, ok := settings[groupName].(map[string]any); ok {
|
|
if value, ok := readInt(group[key]); ok {
|
|
return value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if value, ok := readInt(settings[key]); ok {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func settingsLayoutMeta() map[string]any {
|
|
return map[string]any{
|
|
"layout": "按模块-用途来归类",
|
|
"schema": "module-purpose-v1",
|
|
"description": "Top-level groups are organized by module and purpose for easier maintenance and development.",
|
|
}
|
|
}
|
|
|
|
func ensureDefaultNodeSettings(settings map[string]any) {
|
|
defaults := map[string]string{
|
|
"nodeRole": string(NodeRoleMaster),
|
|
"nodeId": "",
|
|
"syncInterval": "30",
|
|
"trafficFlushInterval": "10",
|
|
}
|
|
|
|
// Ensure both "node" and "other" groups have the defaults for backward
|
|
// compatibility. Old code reads from "other", new code reads from "node".
|
|
for _, groupName := range []string{"node", "other"} {
|
|
group, ok := settings[groupName].(map[string]any)
|
|
if !ok {
|
|
group = make(map[string]any)
|
|
settings[groupName] = group
|
|
}
|
|
for key, value := range defaults {
|
|
if existing, exists := group[key]; !exists || existing == nil {
|
|
group[key] = value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func copyFile(src, dst string) error {
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer in.Close()
|
|
|
|
out, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
_, err = io.Copy(out, in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return out.Sync()
|
|
}
|
|
|
|
func init() {
|
|
if runtime.GOOS != "windows" {
|
|
return
|
|
}
|
|
if os.Getenv("XUI_DB_FOLDER") != "" {
|
|
return
|
|
}
|
|
oldDBFolder := "/etc/x-ui"
|
|
oldDBPath := fmt.Sprintf("%s/%s.db", oldDBFolder, GetName())
|
|
newDBFolder := GetDBFolderPath()
|
|
newDBPath := fmt.Sprintf("%s/%s.db", newDBFolder, GetName())
|
|
_, err := os.Stat(newDBPath)
|
|
if err == nil {
|
|
return // new exists
|
|
}
|
|
_, err = os.Stat(oldDBPath)
|
|
if os.IsNotExist(err) {
|
|
return // old does not exist
|
|
}
|
|
_ = copyFile(oldDBPath, newDBPath) // ignore error
|
|
}
|
|
|
|
// GetDBTypeFromJSON reads the dbType setting directly from the JSON config file.
|
|
// This is needed before the database is initialized. Falls back to "sqlite".
|
|
func GetDBTypeFromJSON() string {
|
|
data, err := os.ReadFile(GetSettingPath())
|
|
if err != nil {
|
|
return "sqlite"
|
|
}
|
|
var settings map[string]any
|
|
if err := json.Unmarshal(data, &settings); err != nil {
|
|
return "sqlite"
|
|
}
|
|
|
|
if dbType := readGroupedString(settings, "dbType"); dbType != "" {
|
|
return dbType
|
|
}
|
|
|
|
return "sqlite"
|
|
}
|
|
|
|
// DBConfig holds MariaDB connection settings read from the JSON config file.
|
|
type DBConfig struct {
|
|
Type string
|
|
Host string
|
|
Port string
|
|
User string
|
|
Password string
|
|
Name string
|
|
}
|
|
|
|
// GetDBConfigFromJSON reads all MariaDB connection settings from the JSON config file.
|
|
func GetDBConfigFromJSON() DBConfig {
|
|
data, err := os.ReadFile(GetSettingPath())
|
|
if err != nil {
|
|
return DBConfig{Type: "sqlite", Host: "127.0.0.1", Port: "3306", Name: "3xui"}
|
|
}
|
|
|
|
var settings map[string]any
|
|
if err := json.Unmarshal(data, &settings); err != nil {
|
|
return DBConfig{Type: "sqlite", Host: "127.0.0.1", Port: "3306", Name: "3xui"}
|
|
}
|
|
|
|
dbType := "sqlite"
|
|
if t := readGroupedString(settings, "dbType"); t != "" {
|
|
dbType = t
|
|
}
|
|
|
|
return DBConfig{
|
|
Type: dbType,
|
|
Host: readGroupedString(settings, "dbHost"),
|
|
Port: readGroupedString(settings, "dbPort"),
|
|
User: readGroupedString(settings, "dbUser"),
|
|
Password: readGroupedString(settings, "dbPassword"),
|
|
Name: readGroupedString(settings, "dbName"),
|
|
}
|
|
}
|
|
|
|
func GetNodeConfigFromJSON() NodeConfig {
|
|
data, err := os.ReadFile(GetSettingPath())
|
|
if err != nil {
|
|
return NodeConfig{Role: NodeRoleMaster, SyncIntervalSeconds: 30, TrafficFlushSeconds: 10}
|
|
}
|
|
|
|
var settings map[string]any
|
|
if err := json.Unmarshal(data, &settings); err != nil {
|
|
return NodeConfig{Role: NodeRoleMaster, SyncIntervalSeconds: 30, TrafficFlushSeconds: 10}
|
|
}
|
|
|
|
role := readGroupedString(settings, "nodeRole")
|
|
if role == "" {
|
|
role = string(NodeRoleMaster)
|
|
}
|
|
|
|
return NodeConfig{
|
|
Role: NodeRole(role),
|
|
NodeID: readGroupedString(settings, "nodeId"),
|
|
SyncIntervalSeconds: readGroupedInt(settings, "syncInterval", 30),
|
|
TrafficFlushSeconds: readGroupedInt(settings, "trafficFlushInterval", 10),
|
|
}
|
|
}
|
|
|
|
func ValidateNodeConfig(nodeCfg NodeConfig, dbCfg DBConfig) error {
|
|
switch nodeCfg.Role {
|
|
case NodeRoleMaster, NodeRoleWorker:
|
|
default:
|
|
return fmt.Errorf("invalid nodeRole %q", nodeCfg.Role)
|
|
}
|
|
if nodeCfg.Role == NodeRoleWorker && nodeCfg.NodeID == "" {
|
|
return fmt.Errorf("worker mode requires nodeId")
|
|
}
|
|
if nodeCfg.Role == NodeRoleWorker && dbCfg.Type != "mariadb" {
|
|
return fmt.Errorf("worker mode requires mariadb")
|
|
}
|
|
if nodeCfg.SyncIntervalSeconds <= 0 {
|
|
return fmt.Errorf("syncInterval must be positive")
|
|
}
|
|
if nodeCfg.TrafficFlushSeconds <= 0 {
|
|
return fmt.Errorf("trafficFlushInterval must be positive")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WriteSettingToJSON writes a single setting key to the JSON config file.
|
|
// It reads the existing file, updates the value, and writes back.
|
|
func WriteSettingToJSON(key, value string) error {
|
|
path := GetSettingPath()
|
|
var settings map[string]any
|
|
data, err := os.ReadFile(path)
|
|
if err == nil {
|
|
if err := json.Unmarshal(data, &settings); err != nil {
|
|
return err
|
|
}
|
|
} else if os.IsNotExist(err) {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
return err
|
|
}
|
|
settings = map[string]any{
|
|
"_meta": settingsLayoutMeta(),
|
|
}
|
|
} else {
|
|
return err
|
|
}
|
|
if _, exists := settings["_meta"]; !exists {
|
|
settings["_meta"] = settingsLayoutMeta()
|
|
}
|
|
ensureDefaultNodeSettings(settings)
|
|
|
|
// Check if the key lives in a nested group
|
|
if groups, ok := settingGroupAliases[key]; ok && len(groups) > 0 {
|
|
group := groups[0]
|
|
if _, exists := settings[group]; !exists {
|
|
settings[group] = make(map[string]any)
|
|
}
|
|
settings[group].(map[string]any)[key] = value
|
|
} else {
|
|
settings[key] = value
|
|
}
|
|
|
|
out, err := json.MarshalIndent(settings, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, out, 0644)
|
|
}
|