package service import ( _ "embed" "encoding/json" "fmt" "net" "reflect" "strconv" "strings" "time" "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/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 = model.DefaultSettingValues(xrayTemplateConfig) // 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) { allSetting := &entity.AllSetting{} t := reflect.TypeOf(allSetting).Elem() v := reflect.ValueOf(allSetting).Elem() fields := reflect_util.GetFields(t) for _, field := range fields { key := field.Tag.Get("json") if key == "" { continue } value, err := s.getString(key) if err != nil { return nil, err } fieldV := v.FieldByName(field.Name) switch fieldV.Kind() { case reflect.String: fieldV.SetString(value) case reflect.Bool: parsed, err := strconv.ParseBool(value) if err != nil { return nil, err } fieldV.SetBool(parsed) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: parsed, err := strconv.ParseInt(value, 10, 64) if err != nil { return nil, err } fieldV.SetInt(parsed) default: return nil, common.NewErrorf("unknown field %v type %v", key, fieldV.Kind()) } } return allSetting, nil } func (s *SettingService) ResetSettings() error { db := database.GetDB() if err := db.Where("1 = 1").Delete(model.Setting{}).Error; err != nil { return err } if err := db.Where("1 = 1").Delete(model.AppSettings{}).Error; err != nil { return err } return nil } func (s *SettingService) getLegacySetting(key string) (*model.Setting, error) { db := database.GetDB() setting := &model.Setting{} err := db.Model(model.Setting{}).Where("key = ?", key).First(setting).Error if err != nil { return nil, err } return setting, nil } func (s *SettingService) saveLegacySetting(key string, value string) error { db := database.GetDB() setting := &model.Setting{} return db.Where("key = ?", key).Assign(model.Setting{ Key: key, Value: value, }).FirstOrCreate(setting).Error } func (s *SettingService) saveSetting(key string, value string) error { repo := settingsRepository{} recognized, err := repo.set(key, value) if err != nil { return err } // Keep shadow write for one release as compatibility fallback. if legacyErr := s.saveLegacySetting(key, value); legacyErr != nil { return legacyErr } if recognized { return nil } return nil } func (s *SettingService) getString(key string) (string, error) { repo := settingsRepository{} if value, recognized, err := repo.get(key); err == nil && recognized { if key == "xrayTemplateConfig" && value == "" { defaultTemplate := defaultValueMap[key] _ = s.saveSetting(key, defaultTemplate) return defaultTemplate, nil } return value, nil } else if err != nil { return "", err } setting, err := s.getLegacySetting(key) if database.IsNotFound(err) { value, ok := defaultValueMap[key] if !ok { return "", common.NewErrorf("key <%v> not in defaultValueMap", key) } return value, nil } else if err != nil { return "", err } return setting.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) { return s.getString("xrayTemplateConfig") } 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 } // LDAP exported getters 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) UpdateAllSetting(allSetting *entity.AllSetting) error { if err := allSetting.CheckValid(); err != nil { return err } v := reflect.ValueOf(allSetting).Elem() t := reflect.TypeOf(allSetting).Elem() fields := reflect_util.GetFields(t) errs := make([]error, 0) for _, field := range fields { key := field.Tag.Get("json") fieldV := v.FieldByName(field.Name) value := fmt.Sprint(fieldV.Interface()) err := s.saveSetting(key, value) if err != nil { errs = append(errs, err) } } return common.Combine(errs...) } 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 }