diff --git a/util/reflect_util/reflect.go b/util/reflect_util/reflect.go new file mode 100644 index 00000000..1fdaec50 --- /dev/null +++ b/util/reflect_util/reflect.go @@ -0,0 +1,21 @@ +package reflect_util + +import "reflect" + +func GetFields(t reflect.Type) []reflect.StructField { + num := t.NumField() + fields := make([]reflect.StructField, 0, num) + for i := 0; i < num; i++ { + fields = append(fields, t.Field(i)) + } + return fields +} + +func GetFieldValues(v reflect.Value) []reflect.Value { + num := v.NumField() + fields := make([]reflect.Value, 0, num) + for i := 0; i < num; i++ { + fields = append(fields, v.Field(i)) + } + return fields +} diff --git a/web/assets/css/custom.css b/web/assets/css/custom.css index fe1b36db..91133894 100644 --- a/web/assets/css/custom.css +++ b/web/assets/css/custom.css @@ -2,6 +2,10 @@ height: 100%; } +.ant-space { + display: block; +} + .ant-layout-sider-zero-width-trigger { display: none; } diff --git a/web/assets/js/model/models.js b/web/assets/js/model/models.js index 144a280d..c30b755d 100644 --- a/web/assets/js/model/models.js +++ b/web/assets/js/model/models.js @@ -88,4 +88,27 @@ class DBInbound { const inbound = this.toInbound(); return inbound.genLink(address, this.remark); } +} + +class AllSetting { + webListen = ""; + webPort = 65432; + webCertFile = ""; + webKeyFile = ""; + webBasePath = "/"; + + xrayTemplateConfig = ""; + + timeLocation = "Asia/Shanghai"; + + constructor(data) { + if (data == null) { + return + } + ObjectUtil.cloneProps(this, data); + } + + equals(other) { + return ObjectUtil.equals(this, other); + } } \ No newline at end of file diff --git a/web/assets/js/util/utils.js b/web/assets/js/util/utils.js index faeced19..154440a4 100644 --- a/web/assets/js/util/utils.js +++ b/web/assets/js/util/utils.js @@ -270,4 +270,18 @@ class ObjectUtil { return obj; } + static equals(a, b) { + for (const key in a) { + if (!a.hasOwnProperty(key)) { + continue; + } + if (!b.hasOwnProperty(key)) { + return false; + } else if (a[key] !== b[key]) { + return false; + } + } + return true + } + } diff --git a/web/controller/xui.go b/web/controller/xui.go index ce63c7b7..3c14c96e 100644 --- a/web/controller/xui.go +++ b/web/controller/xui.go @@ -7,6 +7,7 @@ import ( "strconv" "x-ui/database/model" "x-ui/logger" + "x-ui/web/entity" "x-ui/web/global" "x-ui/web/service" "x-ui/web/session" @@ -17,6 +18,7 @@ type XUIController struct { inboundService service.InboundService xrayService service.XrayService + settingService service.SettingService isNeedXrayRestart atomic.Bool } @@ -39,6 +41,8 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) { g.POST("/inbound/del/:id", a.delInbound) g.POST("/inbound/update/:id", a.updateInbound) g.GET("/setting", a.setting) + g.POST("/setting/all", a.getAllSetting) + g.POST("/setting/update", a.updateSetting) } func (a *XUIController) startTask() { @@ -128,3 +132,23 @@ func (a *XUIController) updateInbound(c *gin.Context) { a.isNeedXrayRestart.Store(true) } } + +func (a *XUIController) getAllSetting(c *gin.Context) { + allSetting, err := a.settingService.GetAllSetting() + if err != nil { + jsonMsg(c, "获取设置", err) + return + } + jsonObj(c, allSetting, nil) +} + +func (a *XUIController) updateSetting(c *gin.Context) { + allSetting := &entity.AllSetting{} + err := c.ShouldBind(allSetting) + if err != nil { + jsonMsg(c, "修改设置", err) + return + } + err = a.settingService.UpdateAllSetting(allSetting) + jsonMsg(c, "修改设置", err) +} diff --git a/web/entity/entity.go b/web/entity/entity.go index ec3e59ed..6a34b944 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -1,5 +1,15 @@ package entity +import ( + "crypto/tls" + "encoding/json" + "net" + "strings" + "time" + "x-ui/util/common" + "x-ui/xray" +) + type Msg struct { Success bool `json:"success"` Msg string `json:"msg"` @@ -15,3 +25,55 @@ type Pager struct { Key string `json:"key"` List interface{} `json:"list"` } + +type AllSetting struct { + WebListen string `json:"webListen" form:"webListen"` + WebPort int `json:"webPort" form:"webPort"` + WebCertFile string `json:"webCertFile" form:"webCertFile"` + WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` + WebBasePath string `json:"webBasePath" form:"webBasePath"` + + XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"` + + TimeLocation string `json:"timeLocation" form:"timeLocation"` +} + +func (s *AllSetting) CheckValid() error { + if s.WebListen != "" { + ip := net.ParseIP(s.WebListen) + if ip == nil { + return common.NewError("web listen is not valid ip:", s.WebListen) + } + } + + if s.WebPort <= 0 || s.WebPort > 65535 { + return common.NewError("web port is not a valid port:", s.WebPort) + } + + if s.WebCertFile != "" || s.WebKeyFile != "" { + _, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile) + if err != nil { + return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.WebCertFile, s.WebKeyFile, err) + } + } + + if !strings.HasPrefix(s.WebBasePath, "/") { + return common.NewErrorf("web base path must start with '/' : <%v>", s.WebBasePath) + } + if !strings.HasSuffix(s.WebBasePath, "/") { + return common.NewErrorf("web base path must end with '/' : <%v>", s.WebBasePath) + } + + xrayConfig := &xray.Config{} + err := json.Unmarshal([]byte(s.XrayTemplateConfig), xrayConfig) + if err != nil { + return common.NewError("xray template config invalid:", err) + } + + _, err = time.LoadLocation(s.TimeLocation) + if err != nil { + return common.NewError("time location not exist:", s.TimeLocation) + } + + return nil +} diff --git a/web/html/xui/common_sider.html b/web/html/xui/common_sider.html index 7aaa2ab7..72ab3da3 100644 --- a/web/html/xui/common_sider.html +++ b/web/html/xui/common_sider.html @@ -5,7 +5,7 @@ - 账号列表 + 入站列表 diff --git a/web/html/xui/component/setting.html b/web/html/xui/component/setting.html new file mode 100644 index 00000000..2b6f6a46 --- /dev/null +++ b/web/html/xui/component/setting.html @@ -0,0 +1,29 @@ +{{define "component/settingListItem"}} + + + + + + + + + + + + +{{end}} + +{{define "component/setting"}} + +{{end}} \ No newline at end of file diff --git a/web/html/xui/inbounds.html b/web/html/xui/inbounds.html index 1185b76e..212bf816 100644 --- a/web/html/xui/inbounds.html +++ b/web/html/xui/inbounds.html @@ -2,8 +2,10 @@ {{template "head" .}} + + + {{ template "commonSider" . }} + + + + + 保存配置 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{template "js" .}} +{{template "component/setting"}} + + + \ No newline at end of file diff --git a/web/service/setting.go b/web/service/setting.go index 70767704..869fe41a 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -2,22 +2,112 @@ package service import ( _ "embed" + "errors" + "fmt" + "reflect" "strconv" "strings" "time" "x-ui/database" "x-ui/database/model" "x-ui/logger" + "x-ui/util/common" "x-ui/util/random" + "x-ui/util/reflect_util" + "x-ui/web/entity" ) //go:embed config.json var xrayTemplateConfig string +var defaultValueMap = map[string]string{ + "xrayTemplateConfig": xrayTemplateConfig, + "webListen": "", + "webPort": "65432", + "webCertFile": "", + "webKeyFile": "", + "secret": random.Seq(32), + "webBasePath": "/", + "timeLocation": "Asia/Shanghai", +} + type SettingService struct { } -func (s *SettingService) ClearSetting() error { +func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) { + db := database.GetDB() + settings := make([]*model.Setting, 0) + err := db.Model(model.Setting{}).Find(&settings).Error + if err != nil { + return nil, err + } + allSetting := &entity.AllSetting{} + t := reflect.TypeOf(allSetting).Elem() + 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, 32) + if err != nil { + return err + } + fieldV.SetInt(n) + case string: + fieldV.SetString(value) + 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) ResetSettings() error { db := database.GetDB() return db.Delete(model.Setting{}).Error } @@ -48,18 +138,22 @@ func (s *SettingService) saveSetting(key string, value string) error { return db.Save(setting).Error } -func (s *SettingService) getString(key string, defaultValue string) (string, error) { +func (s *SettingService) getString(key string) (string, error) { setting, err := s.getSetting(key) if database.IsNotFound(err) { - return defaultValue, nil + 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) getInt(key string, defaultValue int) (int, error) { - str, err := s.getString(key, strconv.Itoa(defaultValue)) +func (s *SettingService) getInt(key string) (int, error) { + str, err := s.getString(key) if err != nil { return 0, err } @@ -67,29 +161,28 @@ func (s *SettingService) getInt(key string, defaultValue int) (int, error) { } func (s *SettingService) GetXrayConfigTemplate() (string, error) { - return s.getString("xray_template_config", xrayTemplateConfig) + return s.getString("xrayTemplateConfig") } func (s *SettingService) GetListen() (string, error) { - return s.getString("web_listen", "") + return s.getString("webListen") } func (s *SettingService) GetPort() (int, error) { - return s.getInt("web_port", 65432) + return s.getInt("webPort") } func (s *SettingService) GetCertFile() (string, error) { - return s.getString("web_cert_file", "") + return s.getString("webCertFile") } func (s *SettingService) GetKeyFile() (string, error) { - return s.getString("web_key_file", "") + return s.getString("webKeyFile") } func (s *SettingService) GetSecret() ([]byte, error) { - seq := random.Seq(32) - secret, err := s.getString("secret", seq) - if secret == seq { + secret, err := s.getString("secret") + if secret == defaultValueMap["secret"] { err := s.saveSetting("secret", secret) if err != nil { logger.Warning("save secret failed:", err) @@ -99,7 +192,7 @@ func (s *SettingService) GetSecret() ([]byte, error) { } func (s *SettingService) GetBasePath() (string, error) { - basePath, err := s.getString("web_base_path", "/") + basePath, err := s.getString("webBasePath") if err != nil { return "", err } @@ -113,15 +206,36 @@ func (s *SettingService) GetBasePath() (string, error) { } func (s *SettingService) GetTimeLocation() (*time.Location, error) { - defaultLocation := "Asia/Shanghai" - l, err := s.getString("time_location", defaultLocation) + 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) 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...) +}