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" ) //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": "", "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": "", } // 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 } var settings map[string]string if err := json.Unmarshal(data, &settings); 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. func saveSettings(settings map[string]string) error { data, err := json.MarshalIndent(settings, "", " ") 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() setting := &model.Setting{} err := db.Model(model.Setting{}).Where("key = ?", "xrayTemplateConfig").First(setting).Error if database.IsNotFound(err) { return db.Create(&model.Setting{ Key: "xrayTemplateConfig", Value: value, }).Error } if err != nil { return err } setting.Value = value return db.Save(setting).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) 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) 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) UpdateAllSetting(allSetting *entity.AllSetting) 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") fieldV := v.FieldByName(field.Name) settings[key] = fmt.Sprint(fieldV.Interface()) } 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() }, "subTitle": func() (any, error) { return s.GetSubTitle() }, "subURI": func() (any, error) { return s.GetSubURI() }, "subJsonURI": func() (any, error) { return s.GetSubJsonURI() }, "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 } } if (subEnable && result["subURI"].(string) == "") || (subJsonEnable && result["subJsonURI"].(string) == "") { subURI := "" subTitle, _ := s.GetSubTitle() subPort, _ := s.GetSubPort() subPath, _ := s.GetSubPath() subJsonPath, _ := s.GetSubJsonPath() 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 } } return result, nil }