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...)
+}