mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-30 09:34:21 +00:00
60c54827aa
* feat(ldap): add InsecureSkipVerify field and tlsConfig helper Extract the inline TLS config at both LDAPS dial sites (FetchVlessFlags, AuthenticateUser) into a tlsConfig(cfg) helper, and add a new Config.InsecureSkipVerify bool that flows through to tls.Config.InsecureSkipVerify. This unblocks enterprise environments (e.g. Microsoft AD CS with internal CAs) where the server certificate chain cannot be imported into the system trust store. Behavior is identical when InsecureSkipVerify is false (the default) - pure refactor + plumbing. The helper is unit-testable without a live server, which is why it is extracted. Closes https://github.com/MHSanaei/3x-ui/issues/5538 * feat(settings): add LdapInsecureSkipVerify setting Plumb the new LDAP skip-TLS-verify toggle through the settings stack: - AllSetting struct field (json/form tag: ldapInsecureSkipVerify) - defaultValueMap default ("false") - GetLdapInsecureSkipVerify() getter - ldap_sync_job wiring into ldaputil.Config (FetchVlessFlags path) - panel/user.go wiring into ldaputil.Config (AuthenticateUser path; the original issue's file list missed this) Persistence is handled by UpdateAllSetting's reflect loop, matching the existing pattern used by ldapUseTLS (no explicit setter). Closes https://github.com/MHSanaei/3x-ui/issues/5538 * feat(ui): add Skip TLS verification switch in LDAP settings Wire the new ldapInsecureSkipVerify setting into the hand-written frontend model and Zod schema, and render it as a new Switch in GeneralTab right under "Use TLS (LDAPS)". The switch is disabled when TLS is off (the setting is meaningless without LDAPS) and shows an insecure-warning description to make the security implication visible to operators. Also adds a Vitest round-trip test pinning schema acceptance and model default-to-false behavior. Closes https://github.com/MHSanaei/3x-ui/issues/5538 * chore(i18n): add Skip TLS verification strings to all locales Add pages.settings.ldap.skipTlsVerify and skipTlsVerifyDesc to all 13 backend-served translation files, matching the existing repo convention of keeping LDAP keys present in every locale (en-US, fa-IR, ru-RU, zh-CN, zh-TW, pt-BR, ar-EG, uk-UA, id-ID, tr-TR, vi-VN, ja-JP, es-ES). No translation-parity test exists in CI, but every other LDAP key is replicated across all files, so this keeps the invariant intact. Closes https://github.com/MHSanaei/3x-ui/issues/5538 * chore(codegen): regenerate frontend artifacts Regenerate frontend/src/generated/{zod,types,schemas,examples}.ts and frontend/public/openapi.json via `npm run gen` to reflect the new ldapInsecureSkipVerify field. The codegen CI job runs `git diff --exit-code` on these files; failing to commit them would break the build. Closes https://github.com/MHSanaei/3x-ui/issues/5538
1316 lines
36 KiB
Go
1316 lines
36 KiB
Go
package service
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/xlzd/gotp"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/config"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/netproxy"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/random"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/reflect_util"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/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),
|
|
"panelGuid": uuid.NewString(),
|
|
"apiToken": "",
|
|
// Node mTLS material (opt-in). All default empty: the CA + master client
|
|
// cert are minted lazily on first use, and the node-side trust CA is pasted
|
|
// in by the operator. Kept out of entity.AllSetting so private keys never
|
|
// reach the settings UI/export.
|
|
"nodeMtlsCaCertPem": "",
|
|
"nodeMtlsCaKeyPem": "",
|
|
"nodeMtlsClientCertPem": "",
|
|
"nodeMtlsClientKeyPem": "",
|
|
"nodeMtlsClientCAPem": "",
|
|
"webBasePath": normalizeBasePath(getEnv("XUI_INIT_WEB_BASE_PATH", "/")),
|
|
"sessionMaxAge": "360",
|
|
"trustedProxyCIDRs": "127.0.0.1/32,::1/128",
|
|
"pageSize": "25",
|
|
"expireDiff": "0",
|
|
"trafficDiff": "0",
|
|
"remarkTemplate": "{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D",
|
|
"timeLocation": "Local",
|
|
"tgBotEnable": "false",
|
|
"tgBotToken": "",
|
|
"tgBotProxy": "",
|
|
"tgBotAPIServer": "",
|
|
"tgBotChatId": "",
|
|
"tgRunTime": "@daily",
|
|
"tgBotBackup": "false",
|
|
"tgCpu": "80",
|
|
"tgMemory": "80",
|
|
"tgLang": "en-US",
|
|
"twoFactorEnable": "false",
|
|
"twoFactorToken": "",
|
|
"subEnable": "true",
|
|
"subJsonEnable": "false",
|
|
"subTitle": "",
|
|
"subSupportUrl": "",
|
|
"subProfileUrl": "",
|
|
"subAnnounce": "",
|
|
"subEnableRouting": "false",
|
|
"subRoutingRules": "",
|
|
"subHideSettings": "false",
|
|
"subIncyEnableRouting": "false",
|
|
"subIncyRoutingRules": "",
|
|
"subListen": "",
|
|
"subPort": "2096",
|
|
"subPath": "/sub/",
|
|
"subDomain": "",
|
|
"subCertFile": "",
|
|
"subKeyFile": "",
|
|
"subUpdates": "12",
|
|
"subEncrypt": "true",
|
|
"subURI": "",
|
|
"subJsonPath": "/json/",
|
|
"subJsonURI": "",
|
|
"subClashEnable": "false",
|
|
"subClashPath": "/clash/",
|
|
"subClashURI": "",
|
|
"subClashEnableRouting": "false",
|
|
"subClashRules": "",
|
|
"subJsonMux": "",
|
|
"subJsonRules": "",
|
|
"subJsonFinalMask": "",
|
|
"subThemeDir": "",
|
|
"datepicker": "gregorian",
|
|
"warp": "",
|
|
"warpUpdateInterval": "0",
|
|
"nord": "",
|
|
"externalTrafficInformEnable": "false",
|
|
"externalTrafficInformURI": "",
|
|
"restartXrayOnClientDisable": "true",
|
|
"xrayOutboundTestUrl": "https://www.google.com/generate_204",
|
|
"panelOutbound": "",
|
|
"devChannelEnable": "false",
|
|
|
|
// LDAP defaults
|
|
"ldapEnable": "false",
|
|
"ldapHost": "",
|
|
"ldapPort": "389",
|
|
"ldapUseTLS": "false",
|
|
"ldapInsecureSkipVerify": "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",
|
|
|
|
// Event bus — per-subscriber event filtering (empty = all disabled)
|
|
"tgEnabledEvents": "login.attempt,cpu.high",
|
|
"smtpEnabledEvents": "login.attempt,cpu.high",
|
|
"smtpCpu": "80",
|
|
"smtpMemory": "80",
|
|
|
|
// Email (SMTP) notifications
|
|
"smtpEnable": "false",
|
|
"smtpHost": "",
|
|
"smtpPort": "587",
|
|
"smtpUsername": "",
|
|
"smtpPassword": "",
|
|
"smtpTo": "",
|
|
"smtpEncryptionType": "starttls", // no, starttls, tls
|
|
}
|
|
|
|
// 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) {
|
|
db := database.GetDB()
|
|
settings := make([]*model.Setting, 0)
|
|
err := db.Model(model.Setting{}).Not("key = ?", "xrayTemplateConfig").Find(&settings).Error
|
|
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 {
|
|
// Some settings are automatically generated, no need to return to the front end to modify the user
|
|
return nil
|
|
}
|
|
|
|
fieldV := v.FieldByName(field.Name)
|
|
switch t := fieldV.Interface().(type) {
|
|
case int:
|
|
n, err := strconv.ParseInt(effectiveSettingValue(key, value), 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fieldV.SetInt(n)
|
|
case string:
|
|
fieldV.SetString(value)
|
|
case bool:
|
|
fieldV.SetBool(effectiveSettingValue(key, value) == "true")
|
|
default:
|
|
return common.NewErrorf("unknown field %v type %v", key, t)
|
|
}
|
|
return
|
|
}
|
|
|
|
keyMap := map[string]bool{}
|
|
for _, setting := range settings {
|
|
err := setSetting(setting.Key, setting.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keyMap[setting.Key] = true
|
|
}
|
|
|
|
for key, value := range defaultValueMap {
|
|
if keyMap[key] {
|
|
continue
|
|
}
|
|
err := setSetting(key, value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return allSetting, nil
|
|
}
|
|
|
|
func (s *SettingService) GetAllSettingView() (*entity.AllSettingView, error) {
|
|
allSetting, err := s.GetAllSetting()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
view := &entity.AllSettingView{AllSetting: *allSetting}
|
|
view.HasTgBotToken = secretConfigured(allSetting.TgBotToken)
|
|
view.HasTwoFactorToken = secretConfigured(allSetting.TwoFactorToken)
|
|
view.HasLdapPassword = secretConfigured(allSetting.LdapPassword)
|
|
view.HasWarpSecret = secretConfigured(mustString(s.GetWarp()))
|
|
view.HasNordSecret = secretConfigured(mustString(s.GetNord()))
|
|
view.HasSmtpPassword = secretConfigured(allSetting.SmtpPassword)
|
|
var apiTokenCount int64
|
|
if err := database.GetDB().Model(model.ApiToken{}).Where("enabled = ?", true).Count(&apiTokenCount).Error; err == nil {
|
|
view.HasApiToken = apiTokenCount > 0
|
|
}
|
|
view.TgBotToken = ""
|
|
view.TwoFactorToken = ""
|
|
view.LdapPassword = ""
|
|
view.SmtpPassword = ""
|
|
return view, nil
|
|
}
|
|
|
|
func secretConfigured(value string) bool {
|
|
return strings.TrimSpace(value) != ""
|
|
}
|
|
|
|
func mustString(value string, _ error) string {
|
|
return value
|
|
}
|
|
|
|
func getEnv(key, fallback string) string {
|
|
val, ok := os.LookupEnv(key)
|
|
if !ok {
|
|
return fallback
|
|
}
|
|
val = strings.TrimSpace(val)
|
|
if val == "" {
|
|
return fallback
|
|
}
|
|
return val
|
|
}
|
|
|
|
func (s *SettingService) ResetSettings() error {
|
|
db := database.GetDB()
|
|
err := db.Where("1 = 1").Delete(model.Setting{}).Error
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return db.Model(model.User{}).
|
|
Where("1 = 1").Error
|
|
}
|
|
|
|
func (s *SettingService) getSetting(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) saveSetting(key string, value string) error {
|
|
setting, err := s.getSetting(key)
|
|
db := database.GetDB()
|
|
if database.IsNotFound(err) {
|
|
return db.Create(&model.Setting{
|
|
Key: key,
|
|
Value: value,
|
|
}).Error
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
setting.Key = key
|
|
setting.Value = value
|
|
return db.Save(setting).Error
|
|
}
|
|
|
|
func (s *SettingService) getString(key string) (string, error) {
|
|
setting, err := s.getSetting(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 effectiveSettingValue(key, stored string) string {
|
|
if stored == "" {
|
|
if def, ok := defaultValueMap[key]; ok {
|
|
return def
|
|
}
|
|
}
|
|
return stored
|
|
}
|
|
|
|
func (s *SettingService) getBool(key string) (bool, error) {
|
|
str, err := s.getString(key)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return strconv.ParseBool(effectiveSettingValue(key, 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(effectiveSettingValue(key, str))
|
|
}
|
|
|
|
func (s *SettingService) setInt(key string, value int) error {
|
|
return s.setString(key, strconv.Itoa(value))
|
|
}
|
|
|
|
func (s *SettingService) GetWarpLastUpdate() (int64, error) {
|
|
val, err := s.getString("warpLastUpdate")
|
|
if err != nil || val == "" {
|
|
return 0, err
|
|
}
|
|
return strconv.ParseInt(val, 10, 64)
|
|
}
|
|
|
|
func (s *SettingService) SetWarpLastUpdate(val int64) error {
|
|
return s.saveSetting("warpLastUpdate", strconv.FormatInt(val, 10))
|
|
}
|
|
|
|
func (s *SettingService) SetWarpUpdateInterval(val int) error {
|
|
return s.setInt("warpUpdateInterval", val)
|
|
}
|
|
|
|
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 {
|
|
clean, err := SanitizeHTTPURL(url)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.setString("xrayOutboundTestUrl", clean)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// GetPanelOutbound returns the Xray outbound tag the panel's own outbound
|
|
// requests (version checks, Telegram, subscription fetches) are routed through.
|
|
func (s *SettingService) GetPanelOutbound() (string, error) {
|
|
return s.getString("panelOutbound")
|
|
}
|
|
|
|
func (s *SettingService) SetPanelOutbound(tag string) error {
|
|
return s.setString("panelOutbound", tag)
|
|
}
|
|
|
|
// PanelEgressProxyURL resolves the loopback SOCKS bridge that the generated
|
|
// config exposes when a panel outbound is configured (see injectPanelEgress).
|
|
// It returns "" — meaning a direct connection — when the feature is off or
|
|
// the bridge is not present in the running core yet.
|
|
func (s *SettingService) PanelEgressProxyURL() string {
|
|
tag, err := s.GetPanelOutbound()
|
|
if err != nil || tag == "" {
|
|
return ""
|
|
}
|
|
proc := XrayProcess()
|
|
if proc == nil || !proc.IsRunning() {
|
|
logger.Warning("panel outbound [", tag, "] is set but Xray is not running, using a direct connection")
|
|
return ""
|
|
}
|
|
cfg := proc.GetConfig()
|
|
if cfg == nil {
|
|
return ""
|
|
}
|
|
for i := range cfg.InboundConfigs {
|
|
if cfg.InboundConfigs[i].Tag == PanelEgressInboundTag {
|
|
return fmt.Sprintf("socks5://127.0.0.1:%d", cfg.InboundConfigs[i].Port)
|
|
}
|
|
}
|
|
logger.Warning("panel outbound [", tag, "] is set but the egress bridge is not in the running config, using a direct connection")
|
|
return ""
|
|
}
|
|
|
|
func (s *SettingService) NodeEgressProxyURL(nodeID int) string {
|
|
tag := NodeEgressInboundTag(nodeID)
|
|
proc := XrayProcess()
|
|
if proc == nil || !proc.IsRunning() {
|
|
logger.Warning("node outbound [", tag, "] is set but Xray is not running, using a direct connection")
|
|
return ""
|
|
}
|
|
cfg := proc.GetConfig()
|
|
if cfg == nil {
|
|
return ""
|
|
}
|
|
for i := range cfg.InboundConfigs {
|
|
if cfg.InboundConfigs[i].Tag == tag {
|
|
return fmt.Sprintf("socks5://127.0.0.1:%d", cfg.InboundConfigs[i].Port)
|
|
}
|
|
}
|
|
logger.Warning("node outbound [", tag, "] is set but the egress bridge is not in the running config, using a direct connection")
|
|
return ""
|
|
}
|
|
|
|
// NewProxiedHTTPClient returns an HTTP client that routes the panel's own
|
|
// outbound requests through the configured panel outbound (via the loopback
|
|
// SOCKS bridge in the running Xray). When the feature is off or the bridge
|
|
// is unavailable it falls back to a direct client.
|
|
func (s *SettingService) NewProxiedHTTPClient(timeout time.Duration) *http.Client {
|
|
proxyUrl := s.PanelEgressProxyURL()
|
|
client, err := netproxy.NewHTTPClient(proxyUrl, timeout)
|
|
if err != nil {
|
|
logger.Warningf("Invalid panel egress proxy %q, using direct connection: %v", proxyUrl, err)
|
|
return &http.Client{Timeout: timeout}
|
|
}
|
|
return client
|
|
}
|
|
|
|
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) GetTgCpu() (int, error) {
|
|
return s.getInt("tgCpu")
|
|
}
|
|
|
|
func (s *SettingService) GetTgMemory() (int, error) {
|
|
return s.getInt("tgMemory")
|
|
}
|
|
|
|
func (s *SettingService) SetTgMemory(value int) error {
|
|
return s.setInt("tgMemory", value)
|
|
}
|
|
|
|
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) VerifyTwoFactorCode(code string) error {
|
|
enabled, err := s.GetTwoFactorEnable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !enabled {
|
|
return nil
|
|
}
|
|
token, err := s.GetTwoFactorToken()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimSpace(token) == "" || !gotp.NewDefaultTOTP(token).Verify(strings.TrimSpace(code), time.Now().Unix()) {
|
|
return common.NewError("invalid two factor code")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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) GetTrustedProxyCIDRs() (string, error) {
|
|
return s.getString("trustedProxyCIDRs")
|
|
}
|
|
|
|
func (s *SettingService) GetRemarkTemplate() (string, error) {
|
|
return s.getString("remarkTemplate")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// GetPanelGuid returns this panel's stable self-identifier, persisting a
|
|
// freshly generated UUID on first read. It is the globally stable node
|
|
// identity used to attribute online clients and inbounds to the physical
|
|
// node that hosts them across a chain of nodes (#4983), where per-panel
|
|
// autoincrement node ids are meaningless one hop away.
|
|
func (s *SettingService) GetPanelGuid() (string, error) {
|
|
guid, err := s.getString("panelGuid")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if guid == defaultValueMap["panelGuid"] {
|
|
if saveErr := s.saveSetting("panelGuid", guid); saveErr != nil {
|
|
logger.Warning("save panelGuid failed:", saveErr)
|
|
}
|
|
}
|
|
return guid, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
return normalizeBasePath(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)
|
|
location, err = time.LoadLocation(defaultLocation)
|
|
if err != nil {
|
|
logger.Errorf("failed to load default location, using UTC: %v", err)
|
|
return time.UTC, nil
|
|
}
|
|
return location, nil
|
|
}
|
|
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) GetSubHideSettings() (bool, error) {
|
|
return s.getBool("subHideSettings")
|
|
}
|
|
|
|
func (s *SettingService) GetSubIncyEnableRouting() (bool, error) {
|
|
return s.getBool("subIncyEnableRouting")
|
|
}
|
|
|
|
func (s *SettingService) GetSubIncyRoutingRules() (string, error) {
|
|
return s.getString("subIncyRoutingRules")
|
|
}
|
|
|
|
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) 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) 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) GetSubClashEnableRouting() (bool, error) {
|
|
return s.getBool("subClashEnableRouting")
|
|
}
|
|
|
|
func (s *SettingService) GetSubClashRules() (string, error) {
|
|
return s.getString("subClashRules")
|
|
}
|
|
|
|
func (s *SettingService) GetSubJsonMux() (string, error) {
|
|
return s.getString("subJsonMux")
|
|
}
|
|
|
|
func (s *SettingService) GetSubJsonRules() (string, error) {
|
|
return s.getString("subJsonRules")
|
|
}
|
|
|
|
func (s *SettingService) GetSubJsonFinalMask() (string, error) {
|
|
return s.getString("subJsonFinalMask")
|
|
}
|
|
|
|
func (s *SettingService) GetSubThemeDir() (string, error) {
|
|
return s.getString("subThemeDir")
|
|
}
|
|
|
|
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) GetNord() (string, error) {
|
|
return s.getString("nord")
|
|
}
|
|
|
|
func (s *SettingService) SetNord(data string) error {
|
|
return s.setString("nord", 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) GetRestartXrayOnClientDisable() (bool, error) {
|
|
return s.getBool("restartXrayOnClientDisable")
|
|
}
|
|
|
|
func (s *SettingService) SetRestartXrayOnClientDisable(value bool) error {
|
|
return s.setBool("restartXrayOnClientDisable", value)
|
|
}
|
|
|
|
// GetDevChannelEnable reports whether the panel self-update tracks the rolling
|
|
// per-commit dev release instead of the latest stable tag.
|
|
func (s *SettingService) GetDevChannelEnable() (bool, error) {
|
|
return s.getBool("devChannelEnable")
|
|
}
|
|
|
|
func (s *SettingService) SetDevChannelEnable(value bool) error {
|
|
return s.setBool("devChannelEnable", value)
|
|
}
|
|
|
|
// GetIpLimitEnable reports whether the IP-limit feature is available. Always
|
|
// true since the panel enforces limits via the core's online-stats API; on an
|
|
// older core the job falls back to access-log parsing and warns there when the
|
|
// log is missing, so the UI no longer hides the field behind that condition.
|
|
func (s *SettingService) GetIpLimitEnable() (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
// GetAccessLogEnable reports whether an Xray access log is configured. Used by
|
|
// the UI for features that genuinely read the log file (the xray log viewer) —
|
|
// distinct from IP limiting, which works without it.
|
|
func (s *SettingService) GetAccessLogEnable() (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) GetLdapInsecureSkipVerify() (bool, error) {
|
|
return s.getBool("ldapInsecureSkipVerify")
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
// Event bus — per-subscriber event filtering
|
|
|
|
func (s *SettingService) GetTgEnabledEvents() (string, error) {
|
|
return s.getString("tgEnabledEvents")
|
|
}
|
|
|
|
func (s *SettingService) SetTgEnabledEvents(events string) error {
|
|
return s.setString("tgEnabledEvents", events)
|
|
}
|
|
|
|
func (s *SettingService) GetSmtpEnabledEvents() (string, error) {
|
|
return s.getString("smtpEnabledEvents")
|
|
}
|
|
|
|
func (s *SettingService) SetSmtpEnabledEvents(events string) error {
|
|
return s.setString("smtpEnabledEvents", events)
|
|
}
|
|
|
|
// Email (SMTP) settings
|
|
|
|
func (s *SettingService) GetSmtpEnable() (bool, error) {
|
|
return s.getBool("smtpEnable")
|
|
}
|
|
|
|
func (s *SettingService) SetSmtpEnable(value bool) error {
|
|
return s.setBool("smtpEnable", value)
|
|
}
|
|
|
|
func (s *SettingService) GetSmtpHost() (string, error) {
|
|
return s.getString("smtpHost")
|
|
}
|
|
|
|
func (s *SettingService) SetSmtpHost(value string) error {
|
|
return s.setString("smtpHost", value)
|
|
}
|
|
|
|
func (s *SettingService) GetSmtpPort() (int, error) {
|
|
return s.getInt("smtpPort")
|
|
}
|
|
|
|
func (s *SettingService) SetSmtpPort(value int) error {
|
|
return s.setInt("smtpPort", value)
|
|
}
|
|
|
|
func (s *SettingService) GetSmtpUsername() (string, error) {
|
|
return s.getString("smtpUsername")
|
|
}
|
|
|
|
func (s *SettingService) SetSmtpUsername(value string) error {
|
|
return s.setString("smtpUsername", value)
|
|
}
|
|
|
|
func (s *SettingService) GetSmtpPassword() (string, error) {
|
|
return s.getString("smtpPassword")
|
|
}
|
|
|
|
func (s *SettingService) SetSmtpPassword(value string) error {
|
|
return s.setString("smtpPassword", value)
|
|
}
|
|
|
|
func (s *SettingService) GetSmtpTo() (string, error) {
|
|
return s.getString("smtpTo")
|
|
}
|
|
|
|
func (s *SettingService) SetSmtpTo(value string) error {
|
|
return s.setString("smtpTo", value)
|
|
}
|
|
|
|
func (s *SettingService) GetSmtpEncryptionType() (string, error) {
|
|
return s.getString("smtpEncryptionType")
|
|
}
|
|
|
|
func (s *SettingService) SetSmtpEncryptionType(value string) error {
|
|
return s.setString("smtpEncryptionType", value)
|
|
}
|
|
|
|
func (s *SettingService) GetSmtpCpu() (int, error) {
|
|
return s.getInt("smtpCpu")
|
|
}
|
|
|
|
func (s *SettingService) SetSmtpCpu(value int) error {
|
|
return s.setInt("smtpCpu", value)
|
|
}
|
|
|
|
func (s *SettingService) GetSmtpMemory() (int, error) {
|
|
return s.getInt("smtpMemory")
|
|
}
|
|
|
|
func (s *SettingService) SetSmtpMemory(value int) error {
|
|
return s.setInt("smtpMemory", value)
|
|
}
|
|
|
|
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
|
if err := s.preserveRedactedSecrets(allSetting); err != nil {
|
|
return err
|
|
}
|
|
if err := validateSettingsURLs(allSetting); err != nil {
|
|
return err
|
|
}
|
|
if err := allSetting.CheckValid(); err != nil {
|
|
return err
|
|
}
|
|
|
|
v := reflect.ValueOf(allSetting).Elem()
|
|
t := reflect.TypeFor[entity.AllSetting]()
|
|
fields := reflect_util.GetFields(t)
|
|
|
|
db := database.GetDB()
|
|
return db.Transaction(func(tx *gorm.DB) error {
|
|
var existing []*model.Setting
|
|
if err := tx.Find(&existing).Error; err != nil {
|
|
return err
|
|
}
|
|
byKey := make(map[string]*model.Setting, len(existing))
|
|
for _, st := range existing {
|
|
byKey[st.Key] = st
|
|
}
|
|
for _, field := range fields {
|
|
key := field.Tag.Get("json")
|
|
fieldV := v.FieldByName(field.Name)
|
|
value := fmt.Sprint(fieldV.Interface())
|
|
if st, ok := byKey[key]; ok {
|
|
if st.Value == value {
|
|
continue
|
|
}
|
|
st.Value = value
|
|
if err := tx.Save(st).Error; err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if err := tx.Create(&model.Setting{Key: key, Value: value}).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (s *SettingService) preserveRedactedSecrets(allSetting *entity.AllSetting) error {
|
|
if strings.TrimSpace(allSetting.TgBotToken) == "" {
|
|
value, err := s.GetTgBotToken()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
allSetting.TgBotToken = value
|
|
}
|
|
if strings.TrimSpace(allSetting.LdapPassword) == "" {
|
|
value, err := s.GetLdapPassword()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
allSetting.LdapPassword = value
|
|
}
|
|
if allSetting.TwoFactorEnable && strings.TrimSpace(allSetting.TwoFactorToken) == "" {
|
|
value, err := s.GetTwoFactorToken()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
allSetting.TwoFactorToken = value
|
|
}
|
|
if strings.TrimSpace(allSetting.SmtpPassword) == "" {
|
|
value, err := s.GetSmtpPassword()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
allSetting.SmtpPassword = value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateSettingsURLs(allSetting *entity.AllSetting) error {
|
|
if allSetting.ExternalTrafficInformURI != "" {
|
|
u, err := SanitizeHTTPURL(allSetting.ExternalTrafficInformURI)
|
|
if err != nil {
|
|
return common.NewError("external traffic inform URI is invalid:", err)
|
|
}
|
|
allSetting.ExternalTrafficInformURI = u
|
|
}
|
|
if allSetting.TgBotAPIServer != "" {
|
|
u, err := SanitizeHTTPURL(allSetting.TgBotAPIServer)
|
|
if err != nil {
|
|
return common.NewError("telegram API server URL is invalid:", err)
|
|
}
|
|
allSetting.TgBotAPIServer = u
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *SettingService) UpdateSecret(key string, value string) error {
|
|
switch key {
|
|
case "tgBotToken", "ldapPassword", "twoFactorToken":
|
|
return s.saveSetting(key, strings.TrimSpace(value))
|
|
default:
|
|
return common.NewError("secret key is not replaceable:", key)
|
|
}
|
|
}
|
|
|
|
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 + "]"
|
|
}
|
|
|
|
// BuildSubURIBase is shared by GetDefaultSettings (the panel's Client
|
|
// Information page) and the subscription page so both render subscription
|
|
// URLs identically.
|
|
func (s *SettingService) BuildSubURIBase(host string) string {
|
|
subPort, _ := s.GetSubPort()
|
|
subDomain, _ := s.GetSubDomain()
|
|
subKeyFile, _ := s.GetSubKeyFile()
|
|
subCertFile, _ := s.GetSubCertFile()
|
|
subTLS := subKeyFile != "" && subCertFile != ""
|
|
if subDomain == "" {
|
|
subDomain = extractHostname(host)
|
|
}
|
|
scheme := "http"
|
|
if subTLS {
|
|
scheme = "https"
|
|
}
|
|
if (subPort == 443 && subTLS) || (subPort == 80 && !subTLS) {
|
|
return scheme + "://" + subDomain
|
|
}
|
|
return fmt.Sprintf("%s://%s:%d", scheme, subDomain, subPort)
|
|
}
|
|
|
|
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() },
|
|
"subThemeDir": func() (any, error) { return s.GetSubThemeDir() },
|
|
"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() },
|
|
"datepicker": func() (any, error) { return s.GetDatepicker() },
|
|
"ipLimitEnable": func() (any, error) { return s.GetIpLimitEnable() },
|
|
"accessLogEnable": func() (any, error) { return s.GetAccessLogEnable() },
|
|
"webDomain": func() (any, error) { return s.GetWebDomain() },
|
|
"subDomain": func() (any, error) { return s.GetSubDomain() },
|
|
"devChannelEnable": func() (any, error) { return s.GetDevChannelEnable() },
|
|
"isDevBuild": func() (any, error) { return config.IsDevBuild(), nil },
|
|
}
|
|
|
|
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 := s.BuildSubURIBase(host)
|
|
subTitle, _ := s.GetSubTitle()
|
|
subPath, _ := s.GetSubPath()
|
|
subJsonPath, _ := s.GetSubJsonPath()
|
|
subClashPath, _ := s.GetSubClashPath()
|
|
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
|
|
}
|