完成大部分基础功能

This commit is contained in:
sprov
2021-05-30 15:23:48 +08:00
parent a66dff6959
commit 4bae13dcc6
12 changed files with 443 additions and 22 deletions

View File

@@ -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
}

View File

@@ -2,6 +2,10 @@
height: 100%;
}
.ant-space {
display: block;
}
.ant-layout-sider-zero-width-trigger {
display: none;
}

View File

@@ -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);
}
}

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -5,7 +5,7 @@
</a-menu-item>
<a-menu-item key="{{ .base_path }}xui/inbounds">
<a-icon type="user"></a-icon>
<span>账号列表</span>
<span>入站列表</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}xui/setting">
<a-icon type="setting"></a-icon>

View File

@@ -0,0 +1,29 @@
{{define "component/settingListItem"}}
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta :title="title" :description="desc"/>
</a-col>
<a-col :lg="24" :xl="12">
<template v-if="type === 'text'">
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
</template>
<template v-else-if="type === 'number'">
<a-input type="number" :value="value" @input="$emit('input', $event.target.value)"></a-input>
</template>
<template v-else-if="type === 'textarea'">
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 6, maxRows: 6 }"></a-textarea>
</template>
</a-col>
</a-row>
</a-list-item>
{{end}}
{{define "component/setting"}}
<script>
Vue.component('setting-list-item', {
props: ["type", "title", "desc", "value"],
template: `{{template "component/settingListItem"}}`,
});
</script>
{{end}}

View File

@@ -2,8 +2,10 @@
<html lang="en">
{{template "head" .}}
<style>
.ant-layout-content {
margin: 24px 16px;
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
.ant-col-sm-24 {
@@ -133,7 +135,7 @@
delimiters: ['[[', ']]'],
el: '#app',
data: {
ip: location.hostname,
siderDrawer,
spinning: false,
dbInbounds: [],
searchKey: '',

View File

@@ -2,8 +2,10 @@
<html lang="en">
{{template "head" .}}
<style>
.ant-layout-content {
margin: 24px 16px;
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
.ant-col-sm-24 {
@@ -273,6 +275,7 @@
delimiters: ['[[', ']]'],
el: '#app',
data: {
siderDrawer,
status: new Status(),
versionModal,
spinning: false,

125
web/html/xui/setting.html Normal file
View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">
{{template "head" .}}
<style>
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
.ant-col-sm-24 {
margin-top: 10px;
}
.ant-tabs-bar {
margin: 0;
}
.ant-list-item {
display: block;
}
.ant-tabs-top-bar {
background: white;
}
</style>
<body>
<a-layout id="app" v-cloak>
{{ template "commonSider" . }}
<a-layout id="content-layout">
<a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading">
<a-space direction="vertical">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">保存配置</a-button>
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="面板配置">
<a-list item-layout="horizontal" style="background: white">
<setting-list-item type="text" title="面板监听 IP" desc="默认留空监听所有 IP" v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title="面板监听端口" v-model.number="allSetting.webPort"></setting-list-item>
<setting-list-item type="text" title="面板证书公钥文件路径" desc="填写一个 '/' 开头的绝对路径" v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title="面板证书密钥文件路径" desc="填写一个 '/' 开头的绝对路径" v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title="面板 url 根路径" desc="必须以 '/' 开头,以 '/' 结尾" v-model="allSetting.webBasePath"></setting-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="2" tab="用户设置">
<a-form style="background: white; padding: 20px">
<a-form-item label="原用户名">
<a-input></a-input>
</a-form-item>
<a-form-item label="原密码">
<a-input></a-input>
</a-form-item>
<a-form-item label="新用户名">
<a-input></a-input>
</a-form-item>
<a-form-item label="新密码">
<a-input></a-input>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="3" tab="xray 相关设置">
<a-list item-layout="horizontal" style="background: white">
<setting-list-item type="textarea" title="xray 配置模版" desc="以该模版为基础生成最终的 xray 配置文件" v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="4" tab="其他设置">
<a-list item-layout="horizontal" style="background: white">
<setting-list-item type="text" title="时区" desc="定时任务按照该时区的时间运行" v-model="allSetting.timeLocation"></setting-list-item>
</a-list>
</a-tab-pane>
</a-tabs>
</a-space>
</a-spin>
</a-layout-content>
</a-layout>
</a-layout>
{{template "js" .}}
{{template "component/setting"}}
<script>
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
siderDrawer,
spinning: false,
oldAllSetting: new AllSetting(),
allSetting: new AllSetting(),
saveBtnDisable: true,
},
methods: {
loading(spinning = true) {
this.spinning = spinning;
},
async getAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/all");
this.loading(false);
if (msg.success) {
this.oldAllSetting = new AllSetting(msg.obj);
this.allSetting = new AllSetting(msg.obj);
this.saveBtnDisable = true;
}
},
async updateAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/update", this.allSetting);
this.loading(false);
if (msg.success) {
await this.getAllSetting();
}
}
},
async mounted() {
await this.getAllSetting();
while (true) {
await PromiseUtil.sleep(1000);
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
}
},
});
</script>
</body>
</html>

View File

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