package service import ( _ "embed" "encoding/json" "errors" "fmt" "net" "os" "reflect" "strconv" "strings" "time" "github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/common" "github.com/mhsanaei/3x-ui/v2/util/random" "github.com/mhsanaei/3x-ui/v2/util/reflect_util" "github.com/mhsanaei/3x-ui/v2/web/entity" "github.com/mhsanaei/3x-ui/v2/xray" "gorm.io/gorm/clause" ) //go:embed config.json var xrayTemplateConfig string var defaultValueMap = map[string]string{ "xrayTemplateConfig": xrayTemplateConfig, "webListen": "", "webDomain": "", "webPort": "2053", "webCertFile": "", "webKeyFile": "", "secret": random.Seq(32), "webBasePath": "/", "sessionMaxAge": "360", "pageSize": "25", "expireDiff": "0", "trafficDiff": "0", "remarkModel": "-ieo", "timeLocation": "Local", "tgBotEnable": "false", "tgBotToken": "", "tgBotProxy": "", "tgBotAPIServer": "", "tgBotChatId": "", "tgRunTime": "@daily", "tgBotBackup": "false", "tgBotLoginNotify": "true", "tgCpu": "80", "tgLang": "en-US", "twoFactorEnable": "false", "twoFactorToken": "", "subEnable": "true", "subJsonEnable": "false", "subTitle": "", "subSupportUrl": "", "subProfileUrl": "", "subAnnounce": "", "subEnableRouting": "true", "subRoutingRules": "", "subListen": "", "subPort": "2096", "subPath": "/sub/", "subDomain": "", "subCertFile": "", "subKeyFile": "", "subUpdates": "12", "subEncrypt": "true", "subShowInfo": "true", "subURI": "", "subJsonPath": "/json/", "subJsonURI": "", "subJsonFragment": "", "subJsonNoises": "", "subJsonMux": "", "subJsonRules": "", "subClashEnable": "false", "subClashPath": "/clash/", "subClashURI": "", "subClashTemplate": "", "datepicker": "gregorian", "warp": "", "externalTrafficInformEnable": "false", "externalTrafficInformURI": "", "xrayOutboundTestUrl": "https://www.google.com/generate_204", // LDAP defaults "ldapEnable": "false", "ldapHost": "", "ldapPort": "389", "ldapUseTLS": "false", "ldapBindDN": "", "ldapPassword": "", "ldapBaseDN": "", "ldapUserFilter": "(objectClass=person)", "ldapUserAttr": "mail", "ldapVlessField": "vless_enabled", "ldapSyncCron": "@every 1m", "ldapFlagField": "", "ldapTruthyValues": "true,1,yes,on", "ldapInvertFlag": "false", "ldapInboundTags": "", "ldapAutoCreate": "false", "ldapAutoDelete": "false", "ldapDefaultTotalGB": "0", "ldapDefaultExpiryDays": "0", "ldapDefaultLimitIP": "0", // Registration settings "turnstileSiteKey": "", "turnstileSecretKey": "", // Database settings "dbType": "sqlite", "dbHost": "127.0.0.1", "dbPort": "3306", "dbUser": "", "dbPassword": "", "dbName": "3xui", // Node settings "nodeRole": "master", "nodeId": "", "syncInterval": "30", "trafficFlushInterval": "10", // Backup settings "backupEnabled": "false", "backupFrequency": "daily", "backupHour": "3", "backupMaxCount": "10", } // settingGroups defines the nested JSON structure for on-disk settings. // Each group maps nested keys to their flat equivalents in defaultValueMap. var settingGroups = map[string]map[string]string{ "panelNetwork": { "listen": "webListen", "domain": "webDomain", "port": "webPort", "basePath": "webBasePath", }, "panelTLS": { "certFile": "webCertFile", "keyFile": "webKeyFile", }, "panelSecurity": { "sessionMaxAge": "sessionMaxAge", "twoFactorEnable": "twoFactorEnable", "twoFactorToken": "twoFactorToken", "turnstileSiteKey": "turnstileSiteKey", "turnstileSecretKey": "turnstileSecretKey", "secret": "secret", }, "panelUX": { "timeLocation": "timeLocation", "datepicker": "datepicker", "pageSize": "pageSize", "expireDiff": "expireDiff", "trafficDiff": "trafficDiff", "remarkModel": "remarkModel", }, "telegramBot": { "enable": "tgBotEnable", "token": "tgBotToken", "proxy": "tgBotProxy", "apiServer": "tgBotAPIServer", "chatId": "tgBotChatId", "runTime": "tgRunTime", "backup": "tgBotBackup", "loginNotify": "tgBotLoginNotify", "cpu": "tgCpu", "lang": "tgLang", }, "subscriptionNetwork": { "enable": "subEnable", "jsonEnable": "subJsonEnable", "clashEnable": "subClashEnable", "listen": "subListen", "port": "subPort", "path": "subPath", "jsonPath": "subJsonPath", "clashPath": "subClashPath", "domain": "subDomain", "certFile": "subCertFile", "keyFile": "subKeyFile", "updates": "subUpdates", "encrypt": "subEncrypt", "showInfo": "subShowInfo", "uri": "subURI", "jsonURI": "subJsonURI", "clashURI": "subClashURI", }, "subscriptionBranding": { "title": "subTitle", "supportUrl": "subSupportUrl", "profileUrl": "subProfileUrl", "announce": "subAnnounce", }, "subscriptionRouting": { "enableRouting": "subEnableRouting", "routingRules": "subRoutingRules", "jsonFragment": "subJsonFragment", "jsonNoises": "subJsonNoises", "jsonMux": "subJsonMux", "jsonRules": "subJsonRules", }, "ldapConnection": { "enable": "ldapEnable", "host": "ldapHost", "port": "ldapPort", "useTLS": "ldapUseTLS", "bindDN": "ldapBindDN", "password": "ldapPassword", "baseDN": "ldapBaseDN", "userFilter": "ldapUserFilter", "userAttr": "ldapUserAttr", "vlessField": "ldapVlessField", }, "ldapSync": { "syncCron": "ldapSyncCron", "flagField": "ldapFlagField", "truthyValues": "ldapTruthyValues", "invertFlag": "ldapInvertFlag", "inboundTags": "ldapInboundTags", "autoCreate": "ldapAutoCreate", "autoDelete": "ldapAutoDelete", "defaultTotalGB": "ldapDefaultTotalGB", "defaultExpiryDays": "ldapDefaultExpiryDays", "defaultLimitIP": "ldapDefaultLimitIP", }, "systemIntegration": { "externalTrafficInformEnable": "externalTrafficInformEnable", "externalTrafficInformURI": "externalTrafficInformURI", "warp": "warp", "xrayOutboundTestUrl": "xrayOutboundTestUrl", }, "databaseConnection": { "dbType": "dbType", "dbHost": "dbHost", "dbPort": "dbPort", "dbUser": "dbUser", "dbPassword": "dbPassword", "dbName": "dbName", }, "node": { "nodeRole": "nodeRole", "nodeId": "nodeId", "syncInterval": "syncInterval", "trafficFlushInterval": "trafficFlushInterval", }, "backup": { "enabled": "backupEnabled", "frequency": "backupFrequency", "hour": "backupHour", "maxCount": "backupMaxCount", }, } var legacySettingGroups = map[string]map[string]string{ "web": { "listen": "webListen", "domain": "webDomain", "port": "webPort", "certFile": "webCertFile", "keyFile": "webKeyFile", "basePath": "webBasePath", "sessionMaxAge": "sessionMaxAge", }, "tgBot": { "enable": "tgBotEnable", "token": "tgBotToken", "proxy": "tgBotProxy", "apiServer": "tgBotAPIServer", "chatId": "tgBotChatId", "runTime": "tgRunTime", "backup": "tgBotBackup", "loginNotify": "tgBotLoginNotify", "cpu": "tgCpu", "lang": "tgLang", }, "sub": { "enable": "subEnable", "jsonEnable": "subJsonEnable", "clashEnable": "subClashEnable", "title": "subTitle", "supportUrl": "subSupportUrl", "profileUrl": "subProfileUrl", "announce": "subAnnounce", "enableRouting": "subEnableRouting", "routingRules": "subRoutingRules", "listen": "subListen", "port": "subPort", "path": "subPath", "jsonPath": "subJsonPath", "clashPath": "subClashPath", "domain": "subDomain", "certFile": "subCertFile", "keyFile": "subKeyFile", "updates": "subUpdates", "encrypt": "subEncrypt", "showInfo": "subShowInfo", "uri": "subURI", "jsonURI": "subJsonURI", "clashURI": "subClashURI", "jsonFragment": "subJsonFragment", "jsonNoises": "subJsonNoises", "jsonMux": "subJsonMux", "jsonRules": "subJsonRules", }, "ldap": { "enable": "ldapEnable", "host": "ldapHost", "port": "ldapPort", "useTLS": "ldapUseTLS", "bindDN": "ldapBindDN", "password": "ldapPassword", "baseDN": "ldapBaseDN", "userFilter": "ldapUserFilter", "userAttr": "ldapUserAttr", "vlessField": "ldapVlessField", "syncCron": "ldapSyncCron", "flagField": "ldapFlagField", "truthyValues": "ldapTruthyValues", "invertFlag": "ldapInvertFlag", "inboundTags": "ldapInboundTags", "autoCreate": "ldapAutoCreate", "autoDelete": "ldapAutoDelete", "defaultTotalGB": "ldapDefaultTotalGB", "defaultExpiryDays": "ldapDefaultExpiryDays", "defaultLimitIP": "ldapDefaultLimitIP", }, "other": { "timeLocation": "timeLocation", "twoFactorEnable": "twoFactorEnable", "twoFactorToken": "twoFactorToken", "externalTrafficInformEnable": "externalTrafficInformEnable", "externalTrafficInformURI": "externalTrafficInformURI", "turnstileSiteKey": "turnstileSiteKey", "turnstileSecretKey": "turnstileSecretKey", "datepicker": "datepicker", "pageSize": "pageSize", "expireDiff": "expireDiff", "trafficDiff": "trafficDiff", "remarkModel": "remarkModel", "secret": "secret", "warp": "warp", "xrayOutboundTestUrl": "xrayOutboundTestUrl", "dbType": "dbType", "dbHost": "dbHost", "dbPort": "dbPort", "dbUser": "dbUser", "dbPassword": "dbPassword", "dbName": "dbName", }, } func settingsLayoutMeta() map[string]string { return map[string]string{ "layout": "按模块-用途来归类", "schema": "module-purpose-v1", "description": "Top-level groups are organized by module and purpose for easier maintenance and development.", } } // flatToNestedKey maps flat keys to their [group, nestedKey] pair. var flatToNestedKey map[string][2]string func init() { flatToNestedKey = make(map[string][2]string) for group, keys := range settingGroups { for nestedKey, flatKey := range keys { flatToNestedKey[flatKey] = [2]string{group, nestedKey} } } } // expandToNested converts a flat map[string]string to nested map[string]any // using the settingGroups mapping. Keys not in any group are placed at the top level. func expandToNested(flat map[string]string) map[string]any { result := make(map[string]any) result["_meta"] = settingsLayoutMeta() // Initialize all groups for group := range settingGroups { result[group] = make(map[string]string) } // Place each flat key into its group for flatKey, value := range flat { if pair, ok := flatToNestedKey[flatKey]; ok { group, nestedKey := pair[0], pair[1] result[group].(map[string]string)[nestedKey] = value } else { // Ungrouped keys go to top level result[flatKey] = value } } // Remove empty groups for group := range result { if m, ok := result[group].(map[string]string); ok && len(m) == 0 { delete(result, group) } } return result } // flattenNested converts a nested map[string]any (from JSON) to a flat map[string]string. // It uses settingGroups to map nested keys back to flat keys. func flattenNested(nested map[string]any) map[string]string { result := make(map[string]string) for key, val := range nested { switch v := val.(type) { case map[string]any: // This is a group if groupKeys, ok := settingGroups[key]; ok { for nestedKey, flatKey := range groupKeys { if strVal, exists := v[nestedKey]; exists { result[flatKey] = fmt.Sprint(strVal) } } } else if groupKeys, ok := legacySettingGroups[key]; ok { for nestedKey, flatKey := range groupKeys { if strVal, exists := v[nestedKey]; exists { result[flatKey] = fmt.Sprint(strVal) } } } default: // Top-level value (ungrouped key) result[key] = fmt.Sprint(val) } } return result } // tryParseNested detects whether the JSON is nested or flat format and returns a flat map. func tryParseNested(data []byte) (map[string]string, error) { // First try to detect if it's nested by checking for object values var probe map[string]any if err := json.Unmarshal(data, &probe); err != nil { return nil, err } // Check if any value is a nested object (map[string]any) — indicates nested format for _, v := range probe { if _, isNested := v.(map[string]any); isNested { return flattenNested(probe), nil } } // Flat format — all values are strings result := make(map[string]string, len(probe)) for k, v := range probe { result[k] = fmt.Sprint(v) } return result, nil } // loadSettings reads the JSON settings file into a map. // If the file doesn't exist, it creates one from defaultValueMap (excluding xrayTemplateConfig). // If the file exists, missing keys from defaultValueMap are merged in (supports new fields added after install). func loadSettings() (map[string]string, error) { path := config.GetSettingPath() data, err := os.ReadFile(path) if os.IsNotExist(err) { settings := make(map[string]string) for k, v := range defaultValueMap { if k == "xrayTemplateConfig" { continue } settings[k] = v } return settings, saveSettings(settings) } if err != nil { return nil, err } // Detect format: try nested first, fall back to flat settings, err := tryParseNested(data) if err != nil { return nil, fmt.Errorf("failed to parse settings file %s: %w", path, err) } // Merge missing keys from defaults so new fields are picked up on upgrade needsSave := false for k, v := range defaultValueMap { if k == "xrayTemplateConfig" { continue } if _, exists := settings[k]; !exists { settings[k] = v needsSave = true } } if needsSave { if err := saveSettings(settings); err != nil { return nil, fmt.Errorf("failed to save merged settings: %w", err) } } return settings, nil } // saveSettings writes the settings map to the JSON file in nested format. func saveSettings(settings map[string]string) error { nested := expandToNested(settings) data, err := json.MarshalIndent(nested, "", " ") if err != nil { return err } return os.WriteFile(config.GetSettingPath(), data, 0644) } // getXrayTemplateConfigFromDB reads xrayTemplateConfig directly from the database. func getXrayTemplateConfigFromDB() (string, error) { db := database.GetDB() setting := &model.Setting{} err := db.Model(model.Setting{}).Where("`key` = ?", "xrayTemplateConfig").First(setting).Error if err != nil { return "", err } return setting.Value, nil } // saveXrayTemplateConfigToDB writes xrayTemplateConfig directly to the database. func saveXrayTemplateConfigToDB(value string) error { db := database.GetDB() return db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "key"}}, DoUpdates: clause.Assignments(map[string]any{"value": value}), }).Create(&model.Setting{ Key: "xrayTemplateConfig", Value: value, }).Error } // SettingService provides business logic for application settings management. // It handles configuration storage, retrieval, and validation for all system settings. type SettingService struct{} func (s *SettingService) GetDefaultJSONConfig() (any, error) { var jsonData any err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData) if err != nil { return nil, err } return jsonData, nil } func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) { settings, err := loadSettings() if err != nil { return nil, err } allSetting := &entity.AllSetting{} t := reflect.TypeFor[entity.AllSetting]() v := reflect.ValueOf(allSetting).Elem() fields := reflect_util.GetFields(t) setSetting := func(key, value string) (err error) { defer func() { panicErr := recover() if panicErr != nil { err = errors.New(fmt.Sprint(panicErr)) } }() var found bool var field reflect.StructField for _, f := range fields { if f.Tag.Get("json") == key { field = f found = true break } } if !found { return nil } fieldV := v.FieldByName(field.Name) switch t := fieldV.Interface().(type) { case int: n, err := strconv.ParseInt(value, 10, 64) if err != nil { return err } fieldV.SetInt(n) case string: fieldV.SetString(value) case bool: fieldV.SetBool(value == "true") default: return common.NewErrorf("unknown field %v type %v", key, t) } return } keyMap := map[string]bool{} for key, value := range settings { err := setSetting(key, value) if err != nil { return nil, err } keyMap[key] = true } for key, value := range defaultValueMap { if key == "xrayTemplateConfig" { continue } if keyMap[key] { continue } err := setSetting(key, value) if err != nil { return nil, err } } return allSetting, nil } func (s *SettingService) ResetSettings() error { // Delete the JSON settings file err := os.Remove(config.GetSettingPath()) if err != nil && !os.IsNotExist(err) { return err } // Reset certificate settings to empty if err := s.SetCertFile(""); err != nil { return err } return s.SetKeyFile("") } func (s *SettingService) saveSetting(key string, value string) error { settings, err := loadSettings() if err != nil { return err } settings[key] = value return saveSettings(settings) } func (s *SettingService) getString(key string) (string, error) { settings, err := loadSettings() if err != nil { return "", err } value, ok := settings[key] if !ok { defaultValue, hasDefault := defaultValueMap[key] if !hasDefault { return "", common.NewErrorf("key <%v> not in defaultValueMap", key) } return defaultValue, nil } return value, nil } func (s *SettingService) setString(key string, value string) error { return s.saveSetting(key, value) } func (s *SettingService) getBool(key string) (bool, error) { str, err := s.getString(key) if err != nil { return false, err } return strconv.ParseBool(str) } func (s *SettingService) setBool(key string, value bool) error { return s.setString(key, strconv.FormatBool(value)) } func (s *SettingService) getInt(key string) (int, error) { str, err := s.getString(key) if err != nil { return 0, err } return strconv.Atoi(str) } func (s *SettingService) setInt(key string, value int) error { return s.setString(key, strconv.Itoa(value)) } func (s *SettingService) GetXrayConfigTemplate() (string, error) { config, err := getXrayTemplateConfigFromDB() if err != nil { // If not in DB, return the embedded default return xrayTemplateConfig, nil } return config, nil } func (s *SettingService) GetXrayOutboundTestUrl() (string, error) { return s.getString("xrayOutboundTestUrl") } func (s *SettingService) SetXrayOutboundTestUrl(url string) error { return s.setString("xrayOutboundTestUrl", url) } func (s *SettingService) GetListen() (string, error) { return s.getString("webListen") } func (s *SettingService) SetListen(ip string) error { return s.setString("webListen", ip) } func (s *SettingService) GetWebDomain() (string, error) { return s.getString("webDomain") } func (s *SettingService) SetWebDomain(domain string) error { return s.setString("webDomain", domain) } func (s *SettingService) GetTgBotToken() (string, error) { return s.getString("tgBotToken") } func (s *SettingService) SetTgBotToken(token string) error { return s.setString("tgBotToken", token) } func (s *SettingService) GetTgBotProxy() (string, error) { return s.getString("tgBotProxy") } func (s *SettingService) SetTgBotProxy(token string) error { return s.setString("tgBotProxy", token) } func (s *SettingService) GetTgBotAPIServer() (string, error) { return s.getString("tgBotAPIServer") } func (s *SettingService) SetTgBotAPIServer(token string) error { return s.setString("tgBotAPIServer", token) } func (s *SettingService) GetTgBotChatId() (string, error) { return s.getString("tgBotChatId") } func (s *SettingService) SetTgBotChatId(chatIds string) error { return s.setString("tgBotChatId", chatIds) } func (s *SettingService) GetTgbotEnabled() (bool, error) { return s.getBool("tgBotEnable") } func (s *SettingService) SetTgbotEnabled(value bool) error { return s.setBool("tgBotEnable", value) } func (s *SettingService) GetTgbotRuntime() (string, error) { return s.getString("tgRunTime") } func (s *SettingService) SetTgbotRuntime(time string) error { return s.setString("tgRunTime", time) } func (s *SettingService) GetTgBotBackup() (bool, error) { return s.getBool("tgBotBackup") } func (s *SettingService) GetTgBotLoginNotify() (bool, error) { return s.getBool("tgBotLoginNotify") } func (s *SettingService) GetTgCpu() (int, error) { return s.getInt("tgCpu") } func (s *SettingService) GetTgLang() (string, error) { return s.getString("tgLang") } func (s *SettingService) GetTwoFactorEnable() (bool, error) { return s.getBool("twoFactorEnable") } func (s *SettingService) SetTwoFactorEnable(value bool) error { return s.setBool("twoFactorEnable", value) } func (s *SettingService) GetTwoFactorToken() (string, error) { return s.getString("twoFactorToken") } func (s *SettingService) SetTwoFactorToken(value string) error { return s.setString("twoFactorToken", value) } func (s *SettingService) GetPort() (int, error) { return s.getInt("webPort") } func (s *SettingService) SetPort(port int) error { return s.setInt("webPort", port) } func (s *SettingService) SetCertFile(webCertFile string) error { return s.setString("webCertFile", webCertFile) } func (s *SettingService) GetCertFile() (string, error) { return s.getString("webCertFile") } func (s *SettingService) SetKeyFile(webKeyFile string) error { return s.setString("webKeyFile", webKeyFile) } func (s *SettingService) GetKeyFile() (string, error) { return s.getString("webKeyFile") } func (s *SettingService) GetExpireDiff() (int, error) { return s.getInt("expireDiff") } func (s *SettingService) GetTrafficDiff() (int, error) { return s.getInt("trafficDiff") } func (s *SettingService) GetSessionMaxAge() (int, error) { return s.getInt("sessionMaxAge") } func (s *SettingService) GetRemarkModel() (string, error) { return s.getString("remarkModel") } func (s *SettingService) GetSecret() ([]byte, error) { secret, err := s.getString("secret") if secret == defaultValueMap["secret"] { err := s.saveSetting("secret", secret) if err != nil { logger.Warning("save secret failed:", err) } } return []byte(secret), err } func (s *SettingService) SetBasePath(basePath string) error { if !strings.HasPrefix(basePath, "/") { basePath = "/" + basePath } if !strings.HasSuffix(basePath, "/") { basePath += "/" } return s.setString("webBasePath", basePath) } func (s *SettingService) GetBasePath() (string, error) { basePath, err := s.getString("webBasePath") if err != nil { return "", err } if !strings.HasPrefix(basePath, "/") { basePath = "/" + basePath } if !strings.HasSuffix(basePath, "/") { basePath += "/" } return basePath, nil } func (s *SettingService) GetTimeLocation() (*time.Location, error) { l, err := s.getString("timeLocation") if err != nil { return nil, err } location, err := time.LoadLocation(l) if err != nil { defaultLocation := defaultValueMap["timeLocation"] logger.Errorf("location <%v> not exist, using default location: %v", l, defaultLocation) return time.LoadLocation(defaultLocation) } return location, nil } func (s *SettingService) GetSubEnable() (bool, error) { return s.getBool("subEnable") } func (s *SettingService) GetSubJsonEnable() (bool, error) { return s.getBool("subJsonEnable") } func (s *SettingService) GetSubTitle() (string, error) { return s.getString("subTitle") } func (s *SettingService) GetSubSupportUrl() (string, error) { return s.getString("subSupportUrl") } func (s *SettingService) GetSubProfileUrl() (string, error) { return s.getString("subProfileUrl") } func (s *SettingService) GetSubAnnounce() (string, error) { return s.getString("subAnnounce") } func (s *SettingService) GetSubEnableRouting() (bool, error) { return s.getBool("subEnableRouting") } func (s *SettingService) GetSubRoutingRules() (string, error) { return s.getString("subRoutingRules") } func (s *SettingService) GetSubListen() (string, error) { return s.getString("subListen") } func (s *SettingService) GetSubPort() (int, error) { return s.getInt("subPort") } func (s *SettingService) GetSubPath() (string, error) { return s.getString("subPath") } func (s *SettingService) GetSubJsonPath() (string, error) { return s.getString("subJsonPath") } func (s *SettingService) GetSubDomain() (string, error) { return s.getString("subDomain") } func (s *SettingService) SetSubCertFile(subCertFile string) error { return s.setString("subCertFile", subCertFile) } func (s *SettingService) GetSubCertFile() (string, error) { return s.getString("subCertFile") } func (s *SettingService) SetSubKeyFile(subKeyFile string) error { return s.setString("subKeyFile", subKeyFile) } func (s *SettingService) GetSubKeyFile() (string, error) { return s.getString("subKeyFile") } func (s *SettingService) GetSubUpdates() (string, error) { return s.getString("subUpdates") } func (s *SettingService) GetSubEncrypt() (bool, error) { return s.getBool("subEncrypt") } func (s *SettingService) GetSubShowInfo() (bool, error) { return s.getBool("subShowInfo") } func (s *SettingService) GetPageSize() (int, error) { return s.getInt("pageSize") } func (s *SettingService) GetSubURI() (string, error) { return s.getString("subURI") } func (s *SettingService) GetSubJsonURI() (string, error) { return s.getString("subJsonURI") } func (s *SettingService) GetSubJsonFragment() (string, error) { return s.getString("subJsonFragment") } func (s *SettingService) GetSubJsonNoises() (string, error) { return s.getString("subJsonNoises") } func (s *SettingService) GetSubJsonMux() (string, error) { return s.getString("subJsonMux") } func (s *SettingService) GetSubJsonRules() (string, error) { return s.getString("subJsonRules") } func (s *SettingService) GetSubClashEnable() (bool, error) { return s.getBool("subClashEnable") } func (s *SettingService) GetSubClashPath() (string, error) { return s.getString("subClashPath") } func (s *SettingService) GetSubClashURI() (string, error) { return s.getString("subClashURI") } func (s *SettingService) GetSubClashTemplate() (string, error) { return s.getString("subClashTemplate") } func (s *SettingService) GetDatepicker() (string, error) { return s.getString("datepicker") } func (s *SettingService) GetWarp() (string, error) { return s.getString("warp") } func (s *SettingService) SetWarp(data string) error { return s.setString("warp", data) } func (s *SettingService) GetExternalTrafficInformEnable() (bool, error) { return s.getBool("externalTrafficInformEnable") } func (s *SettingService) SetExternalTrafficInformEnable(value bool) error { return s.setBool("externalTrafficInformEnable", value) } func (s *SettingService) GetExternalTrafficInformURI() (string, error) { return s.getString("externalTrafficInformURI") } func (s *SettingService) SetExternalTrafficInformURI(InformURI string) error { return s.setString("externalTrafficInformURI", InformURI) } func (s *SettingService) GetIpLimitEnable() (bool, error) { accessLogPath, err := xray.GetAccessLogPath() if err != nil { return false, err } return (accessLogPath != "none" && accessLogPath != ""), nil } // GetLdapEnable returns whether LDAP is enabled. func (s *SettingService) GetLdapEnable() (bool, error) { return s.getBool("ldapEnable") } func (s *SettingService) GetLdapHost() (string, error) { return s.getString("ldapHost") } func (s *SettingService) GetLdapPort() (int, error) { return s.getInt("ldapPort") } func (s *SettingService) GetLdapUseTLS() (bool, error) { return s.getBool("ldapUseTLS") } func (s *SettingService) GetLdapBindDN() (string, error) { return s.getString("ldapBindDN") } func (s *SettingService) GetLdapPassword() (string, error) { return s.getString("ldapPassword") } func (s *SettingService) GetLdapBaseDN() (string, error) { return s.getString("ldapBaseDN") } func (s *SettingService) GetLdapUserFilter() (string, error) { return s.getString("ldapUserFilter") } func (s *SettingService) GetLdapUserAttr() (string, error) { return s.getString("ldapUserAttr") } func (s *SettingService) GetLdapVlessField() (string, error) { return s.getString("ldapVlessField") } func (s *SettingService) GetLdapSyncCron() (string, error) { return s.getString("ldapSyncCron") } func (s *SettingService) GetLdapFlagField() (string, error) { return s.getString("ldapFlagField") } func (s *SettingService) GetLdapTruthyValues() (string, error) { return s.getString("ldapTruthyValues") } func (s *SettingService) GetLdapInvertFlag() (bool, error) { return s.getBool("ldapInvertFlag") } func (s *SettingService) GetLdapInboundTags() (string, error) { return s.getString("ldapInboundTags") } func (s *SettingService) GetLdapAutoCreate() (bool, error) { return s.getBool("ldapAutoCreate") } func (s *SettingService) GetLdapAutoDelete() (bool, error) { return s.getBool("ldapAutoDelete") } func (s *SettingService) GetLdapDefaultTotalGB() (int, error) { return s.getInt("ldapDefaultTotalGB") } func (s *SettingService) GetLdapDefaultExpiryDays() (int, error) { return s.getInt("ldapDefaultExpiryDays") } func (s *SettingService) GetLdapDefaultLimitIP() (int, error) { return s.getInt("ldapDefaultLimitIP") } func (s *SettingService) GetTurnstileSiteKey() (string, error) { return s.getString("turnstileSiteKey") } func (s *SettingService) GetTurnstileSecretKey() (string, error) { return s.getString("turnstileSecretKey") } func (s *SettingService) SetTurnstileSecretKey(value string) error { return s.setString("turnstileSecretKey", value) } func (s *SettingService) GetBackupEnabled() (bool, error) { return s.getBool("backupEnabled") } func (s *SettingService) GetBackupFrequency() (string, error) { return s.getString("backupFrequency") } func (s *SettingService) GetBackupHour() (int, error) { return s.getInt("backupHour") } func (s *SettingService) GetBackupMaxCount() (int, error) { return s.getInt("backupMaxCount") } func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting, presentKeys map[string]struct{}) error { if err := allSetting.CheckValid(); err != nil { return err } settings, err := loadSettings() if err != nil { return err } v := reflect.ValueOf(allSetting).Elem() t := reflect.TypeFor[entity.AllSetting]() fields := reflect_util.GetFields(t) for _, field := range fields { key := field.Tag.Get("json") if key == "-" || key == "" { continue } if presentKeys != nil { if _, ok := presentKeys[key]; !ok { continue } } fieldV := v.FieldByName(field.Name) settings[key] = fmt.Sprint(fieldV.Interface()) } // DBPassword uses json:"-" to avoid leaking to frontend, handle it via form tag if allSetting.DBPassword != "" { settings["dbPassword"] = allSetting.DBPassword } return saveSettings(settings) } func (s *SettingService) GetDefaultXrayConfig() (any, error) { var jsonData any err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData) if err != nil { return nil, err } return jsonData, nil } func extractHostname(host string) string { h, _, err := net.SplitHostPort(host) // Err is not nil means host does not contain port if err != nil { h = host } ip := net.ParseIP(h) // If it's not an IP, return as is if ip == nil { return h } // If it's an IPv4, return as is if ip.To4() != nil { return h } // IPv6 needs bracketing return "[" + h + "]" } func (s *SettingService) GetDefaultSettings(host string) (any, error) { type settingFunc func() (any, error) settings := map[string]settingFunc{ "expireDiff": func() (any, error) { return s.GetExpireDiff() }, "trafficDiff": func() (any, error) { return s.GetTrafficDiff() }, "pageSize": func() (any, error) { return s.GetPageSize() }, "defaultCert": func() (any, error) { return s.GetCertFile() }, "defaultKey": func() (any, error) { return s.GetKeyFile() }, "tgBotEnable": func() (any, error) { return s.GetTgbotEnabled() }, "subEnable": func() (any, error) { return s.GetSubEnable() }, "subJsonEnable": func() (any, error) { return s.GetSubJsonEnable() }, "subClashEnable": func() (any, error) { return s.GetSubClashEnable() }, "subTitle": func() (any, error) { return s.GetSubTitle() }, "subURI": func() (any, error) { return s.GetSubURI() }, "subJsonURI": func() (any, error) { return s.GetSubJsonURI() }, "subClashURI": func() (any, error) { return s.GetSubClashURI() }, "remarkModel": func() (any, error) { return s.GetRemarkModel() }, "datepicker": func() (any, error) { return s.GetDatepicker() }, "ipLimitEnable": func() (any, error) { return s.GetIpLimitEnable() }, } result := make(map[string]any) for key, fn := range settings { value, err := fn() if err != nil { return "", err } result[key] = value } subEnable := result["subEnable"].(bool) subJsonEnable := false if v, ok := result["subJsonEnable"]; ok { if b, ok2 := v.(bool); ok2 { subJsonEnable = b } } subClashEnable := false if v, ok := result["subClashEnable"]; ok { if b, ok2 := v.(bool); ok2 { subClashEnable = b } } if (subEnable && result["subURI"].(string) == "") || (subJsonEnable && result["subJsonURI"].(string) == "") || (subClashEnable && result["subClashURI"].(string) == "") { subURI := "" subTitle, _ := s.GetSubTitle() subPort, _ := s.GetSubPort() subPath, _ := s.GetSubPath() subJsonPath, _ := s.GetSubJsonPath() subClashPath, _ := s.GetSubClashPath() subDomain, _ := s.GetSubDomain() subKeyFile, _ := s.GetSubKeyFile() subCertFile, _ := s.GetSubCertFile() subTLS := false if subKeyFile != "" && subCertFile != "" { subTLS = true } if subDomain == "" { subDomain = extractHostname(host) } if subTLS { subURI = "https://" } else { subURI = "http://" } if (subPort == 443 && subTLS) || (subPort == 80 && !subTLS) { subURI += subDomain } else { subURI += fmt.Sprintf("%s:%d", subDomain, subPort) } if subEnable && result["subURI"].(string) == "" { result["subURI"] = subURI + subPath } if result["subTitle"].(string) == "" { result["subTitle"] = subTitle } if subJsonEnable && result["subJsonURI"].(string) == "" { result["subJsonURI"] = subURI + subJsonPath } if subClashEnable && result["subClashURI"].(string) == "" { result["subClashURI"] = subURI + subClashPath } } return result, nil } func (s *SettingService) GetDBType() (string, error) { return s.getString("dbType") } func (s *SettingService) SetDBType(value string) error { return s.setString("dbType", value) } func (s *SettingService) GetDBHost() (string, error) { return s.getString("dbHost") } func (s *SettingService) SetDBHost(value string) error { return s.setString("dbHost", value) } func (s *SettingService) GetDBPort() (string, error) { return s.getString("dbPort") } func (s *SettingService) SetDBPort(value string) error { return s.setString("dbPort", value) } func (s *SettingService) GetDBUser() (string, error) { return s.getString("dbUser") } func (s *SettingService) SetDBUser(value string) error { return s.setString("dbUser", value) } func (s *SettingService) GetDBPassword() (string, error) { return s.getString("dbPassword") } func (s *SettingService) SetDBPassword(value string) error { return s.setString("dbPassword", value) } func (s *SettingService) GetDBName() (string, error) { return s.getString("dbName") } func (s *SettingService) SetDBName(value string) error { return s.setString("dbName", value) }