diff --git a/database/db.go b/database/db.go index df0e71c..3273874 100644 --- a/database/db.go +++ b/database/db.go @@ -51,9 +51,9 @@ func InitDB(dbPath string) error { var gormLogger logger.Interface if config.IsDebug() { - gormLogger = logger.Discard - } else { gormLogger = logger.Default + } else { + gormLogger = logger.Discard } c := &gorm.Config{ diff --git a/database/model/model.go b/database/model/model.go index 998bc94..965d2c9 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -1,7 +1,8 @@ package model import ( - "encoding/json" + "fmt" + "x-ui/util/json_util" "x-ui/xray" ) @@ -24,32 +25,32 @@ type User struct { type Inbound struct { Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` - UserId int `json:"user_id" form:"user_id"` - Up int64 `json:"up" form:"up"` - Down int64 `json:"down" form:"down"` + UserId int `json:"-"` + Up int64 `json:"up"` + Down int64 `json:"down"` Remark string `json:"remark" form:"remark"` Enable bool `json:"enable" form:"enable"` - ExpiryTime int64 `json:"expiry_time" form:"expiry_time"` + ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // config part Listen string `json:"listen" form:"listen"` - Port int `json:"port" form:"port"` + Port int `json:"port" form:"port" gorm:"unique"` Protocol Protocol `json:"protocol" form:"protocol"` Settings string `json:"settings" form:"settings"` - StreamSettings string `json:"stream_settings" form:"stream_settings"` - Tag string `json:"tag" form:"tag"` + StreamSettings string `json:"streamSettings" form:"streamSettings"` + Tag string `json:"tag" form:"tag" gorm:"unique"` Sniffing string `json:"sniffing" form:"sniffing"` } func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { return &xray.InboundConfig{ - Listen: json.RawMessage(i.Listen), + Listen: json_util.RawMessage(fmt.Sprintf("\"%s\"", i.Listen)), Port: i.Port, Protocol: string(i.Protocol), - Settings: json.RawMessage(i.Settings), - StreamSettings: json.RawMessage(i.StreamSettings), + Settings: json_util.RawMessage(i.Settings), + StreamSettings: json_util.RawMessage(i.StreamSettings), Tag: i.Tag, - Sniffing: json.RawMessage(i.Sniffing), + Sniffing: json_util.RawMessage(i.Sniffing), } } diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..e4ba673 --- /dev/null +++ b/install.sh @@ -0,0 +1,148 @@ +#!/bin/bash + +red='\033[0;31m' +green='\033[0;32m' +yellow='\033[0;33m' +plain='\033[0m' + +cur_dir=$(pwd) + +# check root +[[ $EUID -ne 0 ]] && echo -e "${red}错误:${plain} 必须使用root用户运行此脚本!\n" && exit 1 + +# check os +if [[ -f /etc/redhat-release ]]; then + release="centos" +elif cat /etc/issue | grep -Eqi "debian"; then + release="debian" +elif cat /etc/issue | grep -Eqi "ubuntu"; then + release="ubuntu" +elif cat /etc/issue | grep -Eqi "centos|red hat|redhat"; then + release="centos" +elif cat /proc/version | grep -Eqi "debian"; then + release="debian" +elif cat /proc/version | grep -Eqi "ubuntu"; then + release="ubuntu" +elif cat /proc/version | grep -Eqi "centos|red hat|redhat"; then + release="centos" +else + echo -e "${red}未检测到系统版本,请联系脚本作者!${plain}\n" && exit 1 +fi + +arch=$(arch) + +if [[ $arch == "x86_64" || $arch == "x64" || $arch == "amd64" ]]; then + arch="amd64" +elif [[ $arch == "aarch64" || $arch == "arm64" ]]; then + arch="arm64" +else + arch="amd64" + echo -e "${red}检测架构失败,使用默认架构: ${arch}${plain}" +fi + +echo "架构: ${arch}" + +if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ] ; then + echo "本软件不支持 32 位系统(x86),请使用 64 位系统(x86_64),如果检测有误,请联系作者" + exit -1 +fi + +os_version="" + +# os version +if [[ -f /etc/os-release ]]; then + os_version=$(awk -F'[= ."]' '/VERSION_ID/{print $3}' /etc/os-release) +fi +if [[ -z "$os_version" && -f /etc/lsb-release ]]; then + os_version=$(awk -F'[= ."]+' '/DISTRIB_RELEASE/{print $2}' /etc/lsb-release) +fi + +if [[ x"${release}" == x"centos" ]]; then + if [[ ${os_version} -le 6 ]]; then + echo -e "${red}请使用 CentOS 7 或更高版本的系统!${plain}\n" && exit 1 + fi +elif [[ x"${release}" == x"ubuntu" ]]; then + if [[ ${os_version} -lt 16 ]]; then + echo -e "${red}请使用 Ubuntu 16 或更高版本的系统!${plain}\n" && exit 1 + fi +elif [[ x"${release}" == x"debian" ]]; then + if [[ ${os_version} -lt 8 ]]; then + echo -e "${red}请使用 Debian 8 或更高版本的系统!${plain}\n" && exit 1 + fi +fi + +install_base() { + if [[ x"${release}" == x"centos" ]]; then + yum install wget curl tar -y + else + apt install wget curl tar -y + fi +} + +install_x-ui() { + systemctl stop x-ui + cd /usr/local/ + if [[ -e /usr/local/x-ui/ ]]; then + rm /usr/local/x-ui/ -rf + fi + + if [ $# == 0 ] ;then + last_version=$(curl -Ls "https://api.github.com/repos/sprov065/x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + if [[ ! -n "$last_version" ]]; then + echo -e "${red}检测 x-ui 版本失败,可能是超出 Github API 限制,请稍后再试,或手动指定 x-ui 版本安装${plain}" + exit 1 + fi + echo -e "检测到 x-ui 最新版本:${last_version},开始安装" + wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/sprov065/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz + if [[ $? -ne 0 ]]; then + echo -e "${red}下载 x-ui 失败,请确保你的服务器能够下载 Github 的文件${plain}" + exit 1 + fi + else + last_version=$1 + url="https://github.com/sprov065/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz" + echo -e "开始安装 x-ui v$1" + wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz ${url} + if [[ $? -ne 0 ]]; then + echo -e "${red}下载 x-ui v$1 失败,请确保此版本存在${plain}" + exit 1 + fi + fi + + tar zxvf x-ui-linux-${arch}.tar.gz + rm x-ui-linux-${arch}.tar.gz -f + cd x-ui + chmod +x x-ui bin/xray-x-ui-linux-${arch} + cp -f x-ui.service /etc/systemd/system/ + systemctl daemon-reload + systemctl enable x-ui + systemctl start x-ui + echo -e "${green}x-ui v${last_version}${plain} 安装完成,面板已启动," + echo -e "" + echo -e "如果是全新安装,默认网页端口为 ${green}54321${plain},用户名和密码默认都是 ${green}admin${plain}" + echo -e "请自行确保此端口没有被其他程序占用,${yellow}并且确保 54321 端口已放行${plain}" +# echo -e "若想将 54321 修改为其它端口,输入 x-ui 命令进行修改,同样也要确保你修改的端口也是放行的" + echo -e "" + echo -e "如果是更新面板,则按你之前的方式访问面板" + echo -e "" + curl -o /usr/bin/x-ui -Ls https://raw.githubusercontent.com/sprov065/x-ui/master/x-ui.sh + chmod +x /usr/bin/x-ui + echo -e "x-ui 管理脚本使用方法: " + echo -e "----------------------------------------------" + echo -e "x-ui - 显示管理菜单 (功能更多)" + echo -e "x-ui start - 启动 x-ui 面板" + echo -e "x-ui stop - 停止 x-ui 面板" + echo -e "x-ui restart - 重启 x-ui 面板" + echo -e "x-ui status - 查看 x-ui 状态" + echo -e "x-ui enable - 设置 x-ui 开机自启" + echo -e "x-ui disable - 取消 x-ui 开机自启" + echo -e "x-ui log - 查看 x-ui 日志" + echo -e "x-ui update - 更新 x-ui 面板" + echo -e "x-ui install - 安装 x-ui 面板" + echo -e "x-ui uninstall - 卸载 x-ui 面板" + echo -e "----------------------------------------------" +} + +echo -e "${green}开始安装${plain}" +install_base +install_x-ui $1 diff --git a/main.go b/main.go index 42f0ef2..4d1f771 100644 --- a/main.go +++ b/main.go @@ -12,8 +12,10 @@ import ( "x-ui/config" "x-ui/database" "x-ui/logger" + "x-ui/v2ui" "x-ui/web" "x-ui/web/global" + "x-ui/web/service" ) // this function call global.setWebServer @@ -46,7 +48,8 @@ func runWebServer() { setWebServer(server) err = server.Start() if err != nil { - panic(err) + log.Println(err) + return } sigCh := make(chan os.Signal, 1) @@ -60,7 +63,8 @@ func runWebServer() { setWebServer(server) err = server.Start() if err != nil { - panic(err) + log.Println(err) + return } } else { continue @@ -68,8 +72,48 @@ func runWebServer() { } } -func v2ui(dbPath string) { - // migrate from v2-ui +func resetSetting() { + err := database.InitDB(config.GetDBPath()) + if err != nil { + fmt.Println(err) + return + } + + settingService := service.SettingService{} + err = settingService.ResetSettings() + if err != nil { + fmt.Println("reset setting failed:", err) + } else { + fmt.Println("reset setting success") + } +} + +func updateSetting(port int, username string, password string) { + err := database.InitDB(config.GetDBPath()) + if err != nil { + fmt.Println(err) + return + } + + settingService := service.SettingService{} + + if port > 0 { + err := settingService.SetPort(port) + if err != nil { + fmt.Println("set port failed:", err) + } else { + fmt.Printf("set port %v success", port) + } + } + if username != "" || password != "" { + userService := service.UserService{} + err := userService.UpdateFirstUser(username, password) + if err != nil { + fmt.Println("set username and password failed:", err) + } else { + fmt.Println("set username and password success") + } + } } func main() { @@ -78,24 +122,77 @@ func main() { return } + var showVersion bool + flag.BoolVar(&showVersion, "v", false, "show version") + runCmd := flag.NewFlagSet("run", flag.ExitOnError) v2uiCmd := flag.NewFlagSet("v2-ui", flag.ExitOnError) var dbPath string v2uiCmd.StringVar(&dbPath, "db", "/etc/v2-ui/v2-ui.db", "set v2-ui db file path") - switch flag.Arg(0) { + settingCmd := flag.NewFlagSet("setting", flag.ExitOnError) + var port int + var username string + var password string + var reset bool + settingCmd.BoolVar(&reset, "reset", false, "reset all setting") + settingCmd.IntVar(&port, "port", 0, "set panel port") + settingCmd.StringVar(&username, "username", "", "set login username") + settingCmd.StringVar(&password, "password", "", "set login password") + + oldUsage := flag.Usage + flag.Usage = func() { + oldUsage() + fmt.Println() + fmt.Println("Commands:") + fmt.Println(" run run web panel") + fmt.Println(" v2-ui migrate form v2-ui") + fmt.Println(" setting set settings") + } + + flag.Parse() + if showVersion { + fmt.Println(config.GetVersion()) + return + } + + switch os.Args[1] { case "run": - runCmd.Parse(os.Args[2:]) + err := runCmd.Parse(os.Args[2:]) + if err != nil { + fmt.Println(err) + return + } runWebServer() case "v2-ui": - v2uiCmd.Parse(os.Args[2:]) - v2ui(dbPath) + err := v2uiCmd.Parse(os.Args[2:]) + if err != nil { + fmt.Println(err) + return + } + err = v2ui.MigrateFromV2UI(dbPath) + if err != nil { + logger.Error("migrate from v2-ui failed:", err) + } + case "setting": + err := settingCmd.Parse(os.Args[2:]) + if err != nil { + fmt.Println(err) + return + } + if reset { + resetSetting() + } else { + updateSetting(port, username, password) + } default: - fmt.Println("excepted 'run' or 'v2-ui' subcommands") + fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands") fmt.Println() runCmd.Usage() fmt.Println() v2uiCmd.Usage() + fmt.Println() + settingCmd.Usage() } } diff --git a/util/json_util/json.go b/util/json_util/json.go index 9f27aee..65ad789 100644 --- a/util/json_util/json.go +++ b/util/json_util/json.go @@ -1,37 +1,24 @@ package json_util import ( - "encoding/json" - "reflect" - "x-ui/util/reflect_util" + "errors" ) -/* -MarshalJSON 特殊处理 json.RawMessage +type RawMessage []byte -当 json.RawMessage 不为 nil 且 len() 为 0 时,MarshalJSON 将会解析报错 -*/ -func MarshalJSON(i interface{}) ([]byte, error) { - m := map[string]interface{}{} - t := reflect.TypeOf(i).Elem() - v := reflect.ValueOf(i).Elem() - fields := reflect_util.GetFields(t) - for _, field := range fields { - key := field.Tag.Get("json") - if key == "" || key == "-" { - continue - } - fieldV := v.FieldByName(field.Name) - value := fieldV.Interface() - switch value.(type) { - case json.RawMessage: - value := value.(json.RawMessage) - if len(value) > 0 { - m[key] = value - } - default: - m[key] = value - } +// MarshalJSON 自定义 json.RawMessage 默认行为 +func (m RawMessage) MarshalJSON() ([]byte, error) { + if len(m) == 0 { + return []byte("null"), nil } - return json.Marshal(m) + return m, nil +} + +// UnmarshalJSON sets *m to a copy of data. +func (m *RawMessage) UnmarshalJSON(data []byte) error { + if m == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + *m = append((*m)[0:0], data...) + return nil } diff --git a/v2ui/v2ui.go b/v2ui/v2ui.go new file mode 100644 index 0000000..2311147 --- /dev/null +++ b/v2ui/v2ui.go @@ -0,0 +1,7 @@ +package v2ui + +import "errors" + +func MigrateFromV2UI(dbPath string) error { + return errors.New("not support right now") +} diff --git a/web/assets/css/custom.css b/web/assets/css/custom.css index 9113389..9a2bf07 100644 --- a/web/assets/css/custom.css +++ b/web/assets/css/custom.css @@ -3,7 +3,7 @@ } .ant-space { - display: block; + width: 100%; } .ant-layout-sider-zero-width-trigger { diff --git a/web/assets/js/model/models.js b/web/assets/js/model/models.js index c30b755..650d247 100644 --- a/web/assets/js/model/models.js +++ b/web/assets/js/model/models.js @@ -84,7 +84,7 @@ class DBInbound { } } - genLink(address="") { + genLink(address = "") { const inbound = this.toInbound(); return inbound.genLink(address, this.remark); } @@ -92,7 +92,7 @@ class DBInbound { class AllSetting { webListen = ""; - webPort = 65432; + webPort = 54321; webCertFile = ""; webKeyFile = ""; webBasePath = "/"; diff --git a/web/assets/js/util/utils.js b/web/assets/js/util/utils.js index 154440a..78beb20 100644 --- a/web/assets/js/util/utils.js +++ b/web/assets/js/util/utils.js @@ -223,6 +223,9 @@ class ObjectUtil { } static cloneProps(dest, src, ...ignoreProps) { + if (dest == null || src == null) { + return; + } const ignoreEmpty = this.isArrEmpty(ignoreProps); for (const key of Object.keys(src)) { if (!src.hasOwnProperty(key)) { diff --git a/web/controller/inbound.go b/web/controller/inbound.go index 5c85472..dc97c16 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -3,7 +3,6 @@ package controller import ( "fmt" "github.com/gin-gonic/gin" - "go.uber.org/atomic" "strconv" "x-ui/database/model" "x-ui/logger" @@ -15,8 +14,6 @@ import ( type InboundController struct { inboundService service.InboundService xrayService service.XrayService - - isNeedXrayRestart atomic.Bool } func NewInboundController(g *gin.RouterGroup) *InboundController { @@ -39,12 +36,12 @@ func (a *InboundController) startTask() { webServer := global.GetWebServer() c := webServer.GetCron() c.AddFunc("@every 10s", func() { - if a.isNeedXrayRestart.Load() { + if a.xrayService.IsNeedRestart() { + a.xrayService.SetIsNeedRestart(false) err := a.xrayService.RestartXray() if err != nil { logger.Error("restart xray failed:", err) } - a.isNeedXrayRestart.Store(false) } }) } @@ -73,7 +70,7 @@ func (a *InboundController) addInbound(c *gin.Context) { err = a.inboundService.AddInbound(inbound) jsonMsg(c, "添加", err) if err == nil { - a.isNeedXrayRestart.Store(true) + a.xrayService.SetIsNeedRestart(true) } } @@ -86,7 +83,7 @@ func (a *InboundController) delInbound(c *gin.Context) { err = a.inboundService.DelInbound(id) jsonMsg(c, "删除", err) if err == nil { - a.isNeedXrayRestart.Store(true) + a.xrayService.SetIsNeedRestart(true) } } @@ -107,6 +104,6 @@ func (a *InboundController) updateInbound(c *gin.Context) { err = a.inboundService.UpdateInbound(inbound) jsonMsg(c, "修改", err) if err == nil { - a.isNeedXrayRestart.Store(true) + a.xrayService.SetIsNeedRestart(true) } } diff --git a/web/controller/server.go b/web/controller/server.go index 328cd54..5cc8087 100644 --- a/web/controller/server.go +++ b/web/controller/server.go @@ -38,28 +38,19 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) { } func (a *ServerController) refreshStatus() { - status := a.serverService.GetStatus(a.lastStatus) - a.lastStatus = status + a.lastStatus = a.serverService.GetStatus(a.lastStatus) } func (a *ServerController) startTask() { webServer := global.GetWebServer() - ctx := webServer.GetCtx() - go func() { - for { - select { - case <-ctx.Done(): - return - default: - } - now := time.Now() - if now.Sub(a.lastGetStatusTime) > time.Minute*3 { - time.Sleep(time.Second * 2) - continue - } - a.refreshStatus() + c := webServer.GetCron() + c.AddFunc("@every 2s", func() { + now := time.Now() + if now.Sub(a.lastGetStatusTime) > time.Minute*3 { + return } - }() + a.refreshStatus() + }) } func (a *ServerController) status(c *gin.Context) { diff --git a/web/controller/setting.go b/web/controller/setting.go index 62ae93a..6e9aaff 100644 --- a/web/controller/setting.go +++ b/web/controller/setting.go @@ -1,13 +1,25 @@ package controller import ( + "errors" "github.com/gin-gonic/gin" + "time" "x-ui/web/entity" "x-ui/web/service" + "x-ui/web/session" ) +type updateUserForm struct { + OldUsername string `json:"oldUsername" form:"oldUsername"` + OldPassword string `json:"oldPassword" form:"oldPassword"` + NewUsername string `json:"newUsername" form:"newUsername"` + NewPassword string `json:"newPassword" form:"newPassword"` +} + type SettingController struct { settingService service.SettingService + userService service.UserService + panelService service.PanelService } func NewSettingController(g *gin.RouterGroup) *SettingController { @@ -21,6 +33,8 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) { g.POST("/all", a.getAllSetting) g.POST("/update", a.updateSetting) + g.POST("/updateUser", a.updateUser) + g.POST("/restartPanel", a.restartPanel) } func (a *SettingController) getAllSetting(c *gin.Context) { @@ -42,3 +56,33 @@ func (a *SettingController) updateSetting(c *gin.Context) { err = a.settingService.UpdateAllSetting(allSetting) jsonMsg(c, "修改设置", err) } + +func (a *SettingController) updateUser(c *gin.Context) { + form := &updateUserForm{} + err := c.ShouldBind(form) + if err != nil { + jsonMsg(c, "修改用户", err) + return + } + user := session.GetLoginUser(c) + if user.Username != form.OldUsername || user.Password != form.OldPassword { + jsonMsg(c, "修改用户", errors.New("原用户名或原密码错误")) + return + } + if form.NewUsername == "" || form.NewPassword == "" { + jsonMsg(c, "修改用户", errors.New("新用户名和新密码不能为空")) + return + } + err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword) + if err == nil { + user.Username = form.NewUsername + user.Password = form.NewPassword + session.SetLoginUser(c, user) + } + jsonMsg(c, "修改用户", err) +} + +func (a *SettingController) restartPanel(c *gin.Context) { + err := a.panelService.RestartPanel(time.Second * 3) + jsonMsg(c, "重启面板", err) +} diff --git a/web/entity/entity.go b/web/entity/entity.go index 6a34b94..99a8af8 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -58,10 +58,10 @@ func (s *AllSetting) CheckValid() error { } if !strings.HasPrefix(s.WebBasePath, "/") { - return common.NewErrorf("web base path must start with '/' : <%v>", s.WebBasePath) + s.WebBasePath = "/" + s.WebBasePath } if !strings.HasSuffix(s.WebBasePath, "/") { - return common.NewErrorf("web base path must end with '/' : <%v>", s.WebBasePath) + s.WebBasePath += "/" } xrayConfig := &xray.Config{} diff --git a/web/html/xui/common_sider.html b/web/html/xui/common_sider.html index 72ab3da..5593f5c 100644 --- a/web/html/xui/common_sider.html +++ b/web/html/xui/common_sider.html @@ -11,10 +11,10 @@ 面板设置 - - - 客户端 - + + + + diff --git a/web/html/xui/inbounds.html b/web/html/xui/inbounds.html index 1eacea4..a0e7aa9 100644 --- a/web/html/xui/inbounds.html +++ b/web/html/xui/inbounds.html @@ -19,7 +19,7 @@ - + Please go to the panel settings as soon as possible to modify the username and password, otherwise there may be a risk of leaking account information @@ -32,11 +32,7 @@ total traffic: - - [[ sizeFormat(total.up + total.down) ]] - + [[ sizeFormat(total.up + total.down) ]] number of accounts: @@ -112,26 +108,21 @@ align: 'center', width: 60, scopedSlots: { customRender: 'traffic' }, - }, { - title: "settings", - align: 'center', - width: 60, - scopedSlots: { customRender: 'settings' }, - }, { - title: "streamSettings", - align: 'center', - width: 60, - scopedSlots: { customRender: 'streamSettings' }, + // }, { + // title: "settings", + // align: 'center', + // width: 60, + // scopedSlots: { customRender: 'settings' }, + // }, { + // title: "streamSettings", + // align: 'center', + // width: 60, + // scopedSlots: { customRender: 'streamSettings' }, }, { title: "enable", align: 'center', width: 60, scopedSlots: { customRender: 'enable' }, - }, { - title: "expiryTime", - align: 'center', - width: 60, - scopedSlots: { customRender: 'expiryTime' }, }, { title: "action", align: 'center', @@ -213,7 +204,7 @@ port: inbound.port, protocol: inbound.protocol, settings: inbound.settings.toString(), - stream_settings: inbound.stream.toString(), + streamSettings: inbound.stream.toString(), sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}', }; await this.submit('/xui/inbound/add', data, inModal); @@ -227,7 +218,7 @@ port: inbound.port, protocol: inbound.protocol, settings: inbound.settings.toString(), - stream_settings: inbound.stream.toString(), + streamSettings: inbound.stream.toString(), sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}', }; await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal); @@ -249,30 +240,13 @@ const link = dbInbound.genLink(address); qrModal.show('二维码', link); }, - resetTraffic(inbound) { - this.submit(`/xui/reset_traffic/${inbound.id}`); - }, - resetAllTraffic() { - this.submit('/xui/reset_all_traffic'); - }, switchEnable(dbInbound) { - const data = { - remark: dbInbound.remark, - enable: dbInbound.enable, - - listen: dbInbound.listen, - port: dbInbound.port, - protocol: dbInbound.protocol, - settings: dbInbound.settings, - stream_settings: dbInbound.stream, - sniffing: dbInbound.sniffing, - }; - this.submit(`/xui/inbound/update/${dbInbound.id}`, data); + this.submit(`/xui/inbound/update/${dbInbound.id}`, dbInbound); }, async submit(url, data, modal) { const msg = await HttpUtil.postWithModal(url, data, modal); if (msg.success) { - this.getDBInbounds(); + await this.getDBInbounds(); } }, }, diff --git a/web/html/xui/setting.html b/web/html/xui/setting.html index 53d40d4..13f0aa3 100644 --- a/web/html/xui/setting.html +++ b/web/html/xui/setting.html @@ -31,7 +31,10 @@ - 保存配置 + + 保存配置 + 重启面板 + @@ -45,16 +48,21 @@ - + - + - + - + + + + 修改 @@ -87,6 +95,7 @@ oldAllSetting: new AllSetting(), allSetting: new AllSetting(), saveBtnDisable: true, + user: {}, }, methods: { loading(spinning = true) { @@ -109,6 +118,33 @@ if (msg.success) { await this.getAllSetting(); } + }, + async updateUser() { + this.loading(true); + const msg = await HttpUtil.post("/xui/setting/updateUser", this.user); + this.loading(false); + if (msg.success) { + this.user = {}; + } + }, + async restartPanel() { + await new Promise(resolve => { + this.$confirm({ + title: '重启面板', + content: '确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息', + okText: '确定', + cancelText: '取消', + onOk: () => resolve(), + }); + }); + this.loading(true); + const msg = await HttpUtil.post("/xui/setting/restartPanel"); + this.loading(false); + if (msg.success) { + this.loading(true); + await PromiseUtil.sleep(5000); + location.reload(); + } } }, async mounted() { diff --git a/web/service/panel.go b/web/service/panel.go new file mode 100644 index 0000000..f90d3e6 --- /dev/null +++ b/web/service/panel.go @@ -0,0 +1,26 @@ +package service + +import ( + "os" + "syscall" + "time" + "x-ui/logger" +) + +type PanelService struct { +} + +func (s *PanelService) RestartPanel(delay time.Duration) error { + p, err := os.FindProcess(syscall.Getpid()) + if err != nil { + return err + } + go func() { + time.Sleep(delay) + err := p.Signal(syscall.SIGHUP) + if err != nil { + logger.Error("send signal SIGHUP failed:", err) + } + }() + return nil +} diff --git a/web/service/server.go b/web/service/server.go index dfd9d6e..802d017 100644 --- a/web/service/server.go +++ b/web/service/server.go @@ -77,7 +77,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status { T: now, } - percents, err := cpu.Percent(time.Second*2, false) + percents, err := cpu.Percent(0, false) if err != nil { logger.Warning("get cpu percent failed:", err) } else { diff --git a/web/service/setting.go b/web/service/setting.go index 869fe41..dd57d0c 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -23,7 +23,7 @@ var xrayTemplateConfig string var defaultValueMap = map[string]string{ "xrayTemplateConfig": xrayTemplateConfig, "webListen": "", - "webPort": "65432", + "webPort": "54321", "webCertFile": "", "webKeyFile": "", "secret": random.Seq(32), @@ -109,7 +109,7 @@ func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) { func (s *SettingService) ResetSettings() error { db := database.GetDB() - return db.Delete(model.Setting{}).Error + return db.Where("1 = 1").Delete(model.Setting{}).Error } func (s *SettingService) getSetting(key string) (*model.Setting, error) { @@ -152,6 +152,10 @@ func (s *SettingService) getString(key string) (string, error) { return setting.Value, nil } +func (s *SettingService) setString(key string, value string) error { + return s.saveSetting(key, value) +} + func (s *SettingService) getInt(key string) (int, error) { str, err := s.getString(key) if err != nil { @@ -160,6 +164,10 @@ func (s *SettingService) getInt(key string) (int, error) { 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") } @@ -172,6 +180,10 @@ 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) GetCertFile() (string, error) { return s.getString("webCertFile") } diff --git a/web/service/user.go b/web/service/user.go index 9f66ae0..969a539 100644 --- a/web/service/user.go +++ b/web/service/user.go @@ -1,6 +1,7 @@ package service import ( + "errors" "gorm.io/gorm" "x-ui/database" "x-ui/database/model" @@ -26,3 +27,33 @@ func (s *UserService) CheckUser(username string, password string) *model.User { } return user } + +func (s *UserService) UpdateUser(id int, username string, password string) error { + db := database.GetDB() + return db.Model(model.User{}). + Where("id = ?", id). + Update("username", username). + Update("password", password). + Error +} + +func (s *UserService) UpdateFirstUser(username string, password string) error { + if username == "" { + return errors.New("username can not be empty") + } else if password == "" { + return errors.New("password can not be empty") + } + db := database.GetDB() + user := &model.User{} + err := db.Model(model.User{}).First(user).Error + if database.IsNotFound(err) { + user.Username = username + user.Password = password + return db.Model(model.User{}).Create(user).Error + } else if err != nil { + return err + } + user.Username = username + user.Password = password + return db.Save(user).Error +} diff --git a/web/service/xray.go b/web/service/xray.go index 3b1de50..832fd13 100644 --- a/web/service/xray.go +++ b/web/service/xray.go @@ -3,6 +3,7 @@ package service import ( "encoding/json" "errors" + "go.uber.org/atomic" "sync" "x-ui/xray" ) @@ -14,6 +15,8 @@ var result string type XrayService struct { inboundService InboundService settingService SettingService + + isNeedXrayRestart atomic.Bool } func (s *XrayService) IsXrayRunning() bool { @@ -84,15 +87,19 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, error) { func (s *XrayService) RestartXray() error { lock.Lock() defer lock.Unlock() - if p != nil { - p.Stop() - } xrayConfig, err := s.GetXrayConfig() if err != nil { return err } + if p != nil { + if p.GetConfig().Equals(xrayConfig) { + return nil + } + p.Stop() + } + p = xray.NewProcess(xrayConfig) result = "" return p.Start() @@ -106,3 +113,11 @@ func (s *XrayService) StopXray() error { } return errors.New("xray is not running") } + +func (s *XrayService) SetIsNeedRestart(needRestart bool) { + s.isNeedXrayRestart.Store(needRestart) +} + +func (s *XrayService) IsNeedRestart() bool { + return s.isNeedXrayRestart.Load() +} diff --git a/web/translation/translate.zh_Hans.toml b/web/translation/translate.zh_Hans.toml index 9828ea5..1c751cf 100644 --- a/web/translation/translate.zh_Hans.toml +++ b/web/translation/translate.zh_Hans.toml @@ -1,12 +1,12 @@ "username" = "用户名" "password" = "密码" "login" = "登录" -"confirm" = "confirm" -"cancel" = "cancel" -"close" = "close" -"copy" = "copy" -"copied" = "copied" -"download" = "download" -"remark" = "remark" -"enable" = "enable" -"protocol" = "protocol" \ No newline at end of file +"confirm" = "确定" +"cancel" = "取消" +"close" = "关闭" +"copy" = "复制" +"copied" = "已复制" +"download" = "下载" +"remark" = "备注" +"enable" = "启用" +"protocol" = "协议" \ No newline at end of file diff --git a/web/translation/translate.zh_Hant.toml b/web/translation/translate.zh_Hant.toml index 34f65dc..ceda43c 100644 --- a/web/translation/translate.zh_Hant.toml +++ b/web/translation/translate.zh_Hant.toml @@ -1,12 +1,12 @@ "username" = "用戶名" "password" = "密碼" "login" = "登錄" -"confirm" = "confirm" -"cancel" = "cancel" -"close" = "close" -"copy" = "copy" -"copied" = "copied" -"download" = "download" -"remark" = "remark" -"enable" = "enable" -"protocol" = "protocol" \ No newline at end of file +"confirm" = "確定" +"cancel" = "取消" +"close" = "關閉" +"copy" = "複製" +"copied" = "已複製" +"download" = "下載" +"remark" = "備註" +"enable" = "啟用" +"protocol" = "協議" \ No newline at end of file diff --git a/web/web.go b/web/web.go index 0c90a75..469fdb9 100644 --- a/web/web.go +++ b/web/web.go @@ -253,10 +253,7 @@ func (s *Server) startTask() { if checkTime < 2 { return } - err := s.xrayService.RestartXray() - if err != nil { - logger.Warning("start xray failed:", err) - } + s.xrayService.SetIsNeedRestart(true) }) go func() { time.Sleep(time.Second * 5) @@ -316,7 +313,8 @@ func (s *Server) Start() (err error) { listenAddr := net.JoinHostPort(listen, strconv.Itoa(port)) var listener net.Listener if certFile != "" || keyFile != "" { - cert, err := tls.LoadX509KeyPair(certFile, keyFile) + var cert tls.Certificate + cert, err = tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return err } diff --git a/x-ui.sh b/x-ui.sh new file mode 100644 index 0000000..f0e2255 --- /dev/null +++ b/x-ui.sh @@ -0,0 +1,493 @@ +#!/bin/bash + +red='\033[0;31m' +green='\033[0;32m' +yellow='\033[0;33m' +plain='\033[0m' + +# check root +[[ $EUID -ne 0 ]] && echo -e "${red}错误: ${plain} 必须使用root用户运行此脚本!\n" && exit 1 + +# check os +if [[ -f /etc/redhat-release ]]; then + release="centos" +elif cat /etc/issue | grep -Eqi "debian"; then + release="debian" +elif cat /etc/issue | grep -Eqi "ubuntu"; then + release="ubuntu" +elif cat /etc/issue | grep -Eqi "centos|red hat|redhat"; then + release="centos" +elif cat /proc/version | grep -Eqi "debian"; then + release="debian" +elif cat /proc/version | grep -Eqi "ubuntu"; then + release="ubuntu" +elif cat /proc/version | grep -Eqi "centos|red hat|redhat"; then + release="centos" +else + echo -e "${red}未检测到系统版本,请联系脚本作者!${plain}\n" && exit 1 +fi + +os_version="" + +# os version +if [[ -f /etc/os-release ]]; then + os_version=$(awk -F'[= ."]' '/VERSION_ID/{print $3}' /etc/os-release) +fi +if [[ -z "$os_version" && -f /etc/lsb-release ]]; then + os_version=$(awk -F'[= ."]+' '/DISTRIB_RELEASE/{print $2}' /etc/lsb-release) +fi + +if [[ x"${release}" == x"centos" ]]; then + if [[ ${os_version} -le 6 ]]; then + echo -e "${red}请使用 CentOS 7 或更高版本的系统!${plain}\n" && exit 1 + fi +elif [[ x"${release}" == x"ubuntu" ]]; then + if [[ ${os_version} -lt 16 ]]; then + echo -e "${red}请使用 Ubuntu 16 或更高版本的系统!${plain}\n" && exit 1 + fi +elif [[ x"${release}" == x"debian" ]]; then + if [[ ${os_version} -lt 8 ]]; then + echo -e "${red}请使用 Debian 8 或更高版本的系统!${plain}\n" && exit 1 + fi +fi + +confirm() { + if [[ $# > 1 ]]; then + echo && read -p "$1 [默认$2]: " temp + if [[ x"${temp}" == x"" ]]; then + temp=$2 + fi + else + read -p "$1 [y/n]: " temp + fi + if [[ x"${temp}" == x"y" || x"${temp}" == x"Y" ]]; then + return 0 + else + return 1 + fi +} + +confirm_restart() { + confirm "是否重启面板,重启面板也会重启 xray" "y" + if [[ $? == 0 ]]; then + restart + else + show_menu + fi +} + +before_show_menu() { + echo && echo -n -e "${yellow}按回车返回主菜单: ${plain}" && read temp + show_menu +} + +install() { + bash <(curl -Ls https://blog.sprov.xyz/x-ui.sh) + if [[ $? == 0 ]]; then + if [[ $# == 0 ]]; then + start + else + start 0 + fi + fi +} + +update() { + confirm "本功能会强制重装当前最新版,数据不会丢失,是否继续?" "n" + if [[ $? != 0 ]]; then + echo -e "${red}已取消${plain}" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 0 + fi + bash <(curl -Ls https://blog.sprov.xyz/x-ui.sh) + if [[ $? == 0 ]]; then + echo -e "${green}更新完成,已自动重启面板${plain}" + exit 0 + fi +} + +uninstall() { + confirm "确定要卸载面板吗,xray 也会卸载?" "n" + if [[ $? != 0 ]]; then + if [[ $# == 0 ]]; then + show_menu + fi + return 0 + fi + systemctl stop x-ui + systemctl disable x-ui + rm /etc/systemd/system/x-ui.service -f + systemctl daemon-reload + systemctl reset-failed + rm /etc/x-ui/ -rf + rm /usr/local/x-ui/ -rf + + echo "" + echo -e "卸载成功,如果你想删除此脚本,则退出脚本后运行 ${green}rm /usr/bin/x-ui -f${plain} 进行删除" + echo "" + echo -e "Telegram 群组: ${green}https://t.me/sprov_blog${plain}" + echo -e "Github issues: ${green}https://github.com/sprov065/x-ui/issues${plain}" + echo -e "博客: ${green}https://blog.sprov.xyz/x-ui${plain}" + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +reset_user() { + confirm "确定要将用户名和密码重置为 admin 吗" "n" + if [[ $? != 0 ]]; then + if [[ $# == 0 ]]; then + show_menu + fi + return 0 + fi + /usr/local/x-ui/x-ui setting -username admin -password admin + echo -e "用户名和密码已重置为 ${green}admin${plain},现在请重启面板" + confirm_restart +} + +reset_config() { + confirm "确定要重置所有面板设置吗,账号数据不会丢失,用户名和密码不会改变" "n" + if [[ $? != 0 ]]; then + if [[ $# == 0 ]]; then + show_menu + fi + return 0 + fi + /usr/local/x-ui/x-ui setting -reset + echo -e "所有面板设置已重置为默认值,现在请重启面板,并使用默认的 ${green}54321${plain} 端口访问面板" + confirm_restart +} + +set_port() { + echo && echo -n -e "输入端口号[1-65535]: " && read port + if [[ -z "${port}" ]]; then + echo -e "${yellow}已取消${plain}" + before_show_menu + else + /usr/local/x-ui/x-ui setting -port ${port} + echo -e "设置端口完毕,现在请重启面板,并使用新设置的端口 ${green}${port}${plain} 访问面板" + confirm_restart + fi +} + +start() { + check_status + if [[ $? == 0 ]]; then + echo "" + echo -e "${green}面板已运行,无需再次启动,如需重启请选择重启${plain}" + else + systemctl start x-ui + sleep 2 + check_status + if [[ $? == 0 ]]; then + echo -e "${green}x-ui 启动成功${plain}" + else + echo -e "${red}面板启动失败,可能是因为启动时间超过了两秒,请稍后查看日志信息${plain}" + fi + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +stop() { + check_status + if [[ $? == 1 ]]; then + echo "" + echo -e "${green}面板已停止,无需再次停止${plain}" + else + systemctl stop x-ui + sleep 2 + check_status + if [[ $? == 1 ]]; then + echo -e "${green}x-ui 与 xray 停止成功${plain}" + else + echo -e "${red}面板停止失败,可能是因为停止时间超过了两秒,请稍后查看日志信息${plain}" + fi + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +restart() { + systemctl restart x-ui + sleep 2 + check_status + if [[ $? == 0 ]]; then + echo -e "${green}x-ui 与 xray 重启成功${plain}" + else + echo -e "${red}面板重启失败,可能是因为启动时间超过了两秒,请稍后查看日志信息${plain}" + fi + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +status() { + systemctl status x-ui -l + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +enable() { + systemctl enable x-ui + if [[ $? == 0 ]]; then + echo -e "${green}x-ui 设置开机自启成功${plain}" + else + echo -e "${red}x-ui 设置开机自启失败${plain}" + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +disable() { + systemctl disable x-ui + if [[ $? == 0 ]]; then + echo -e "${green}x-ui 取消开机自启成功${plain}" + else + echo -e "${red}x-ui 取消开机自启失败${plain}" + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +show_log() { + journalctl -u x-ui.service -e --no-pager -f + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +install_bbr() { + bash <(curl -L -s https://raw.githubusercontent.com/sprov065/blog/master/bbr.sh) + echo "" + before_show_menu +} + +update_shell() { + wget -O /usr/bin/x-ui -N --no-check-certificate https://github.com/sprov065/x-ui/raw/master/x-ui.sh + if [[ $? != 0 ]]; then + echo "" + echo -e "${red}下载脚本失败,请检查本机能否连接 Github${plain}" + before_show_menu + else + chmod +x /usr/bin/x-ui + echo -e "${green}升级脚本成功,请重新运行脚本${plain}" && exit 0 + fi +} + +# 0: running, 1: not running, 2: not installed +check_status() { + if [[ ! -f /etc/systemd/system/x-ui.service ]]; then + return 2 + fi + temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1) + if [[ x"${temp}" == x"running" ]]; then + return 0 + else + return 1 + fi +} + +check_enabled() { + temp=$(systemctl is-enabled x-ui) + if [[ x"${temp}" == x"enabled" ]]; then + return 0 + else + return 1; + fi +} + +check_uninstall() { + check_status + if [[ $? != 2 ]]; then + echo "" + echo -e "${red}面板已安装,请不要重复安装${plain}" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 1 + else + return 0 + fi +} + +check_install() { + check_status + if [[ $? == 2 ]]; then + echo "" + echo -e "${red}请先安装面板${plain}" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 1 + else + return 0 + fi +} + +show_status() { + check_status + case $? in + 0) + echo -e "面板状态: ${green}已运行${plain}" + show_enable_status + ;; + 1) + echo -e "面板状态: ${yellow}未运行${plain}" + show_enable_status + ;; + 2) + echo -e "面板状态: ${red}未安装${plain}" + esac + show_xray_status +} + +show_enable_status() { + check_enabled + if [[ $? == 0 ]]; then + echo -e "是否开机自启: ${green}是${plain}" + else + echo -e "是否开机自启: ${red}否${plain}" + fi +} + +check_xray_status() { + count=$(ps -ef | grep "xray-linux" | grep -v "grep" | wc -l) + if [[ count -ne 0 ]]; then + return 0 + else + return 1 + fi +} + +show_xray_status() { + check_xray_status + if [[ $? == 0 ]]; then + echo -e "xray 状态: ${green}运行${plain}" + else + echo -e "xray 状态: ${red}未运行${plain}" + fi +} + +show_usage() { + echo "x-ui 管理脚本使用方法: " + echo "------------------------------------------" + echo "x-ui - 显示管理菜单 (功能更多)" + echo "x-ui start - 启动 x-ui 面板" + echo "x-ui stop - 停止 x-ui 面板" + echo "x-ui restart - 重启 x-ui 面板" + echo "x-ui status - 查看 x-ui 状态" + echo "x-ui enable - 设置 x-ui 开机自启" + echo "x-ui disable - 取消 x-ui 开机自启" + echo "x-ui log - 查看 x-ui 日志" + echo "x-ui update - 更新 x-ui 面板" + echo "x-ui install - 安装 x-ui 面板" + echo "x-ui uninstall - 卸载 x-ui 面板" + echo "------------------------------------------" +} + +show_menu() { + echo -e " + ${green}x-ui 面板管理脚本${plain} +--- https://blog.sprov.xyz/x-ui --- + ${green}0.${plain} 退出脚本 +———————————————— + ${green}1.${plain} 安装 x-ui + ${green}2.${plain} 更新 x-ui + ${green}3.${plain} 卸载 x-ui +———————————————— + ${green}4.${plain} 重置用户名密码 + ${green}5.${plain} 重置面板设置 + ${green}6.${plain} 设置面板端口 +———————————————— + ${green}7.${plain} 启动 x-ui + ${green}8.${plain} 停止 x-ui + ${green}9.${plain} 重启 x-ui + ${green}10.${plain} 查看 x-ui 状态 + ${green}11.${plain} 查看 x-ui 日志 +———————————————— + ${green}12.${plain} 设置 x-ui 开机自启 + ${green}13.${plain} 取消 x-ui 开机自启 +———————————————— + ${green}14.${plain} 一键安装 bbr (最新内核) + " + show_status + echo && read -p "请输入选择 [0-14]: " num + + case "${num}" in + 0) exit 0 + ;; + 1) check_uninstall && install + ;; + 2) check_install && update + ;; + 3) check_install && uninstall + ;; + 4) check_install && reset_user + ;; + 5) check_install && reset_config + ;; + 6) check_install && set_port + ;; + 7) check_install && start + ;; + 8) check_install && stop + ;; + 9) check_install && restart + ;; + 10) check_install && status + ;; + 11) check_install && show_log + ;; + 12) check_install && enable + ;; + 13) check_install && disable + ;; + 14) install_bbr + ;; + *) echo -e "${red}请输入正确的数字 [0-14]${plain}" + ;; + esac +} + + +if [[ $# > 0 ]]; then + case $1 in + "start") check_install 0 && start 0 + ;; + "stop") check_install 0 && stop 0 + ;; + "restart") check_install 0 && restart 0 + ;; + "status") check_install 0 && status 0 + ;; + "enable") check_install 0 && enable 0 + ;; + "disable") check_install 0 && disable 0 + ;; + "log") check_install 0 && show_log 0 + ;; + "update") check_install 0 && update 0 + ;; + "install") check_uninstall 0 && install 0 + ;; + "uninstall") check_install 0 && uninstall 0 + ;; + *) show_usage + esac +else + show_menu +fi diff --git a/xray/config.go b/xray/config.go index 507ca17..cc63ca4 100644 --- a/xray/config.go +++ b/xray/config.go @@ -1,24 +1,62 @@ package xray import ( - "encoding/json" + "bytes" "x-ui/util/json_util" ) type Config struct { - LogConfig json.RawMessage `json:"log"` - RouterConfig json.RawMessage `json:"routing"` - DNSConfig json.RawMessage `json:"dns"` - InboundConfigs []InboundConfig `json:"inbounds"` - OutboundConfigs json.RawMessage `json:"outbounds"` - Transport json.RawMessage `json:"transport"` - Policy json.RawMessage `json:"policy"` - API json.RawMessage `json:"api"` - Stats json.RawMessage `json:"stats"` - Reverse json.RawMessage `json:"reverse"` - FakeDNS json.RawMessage `json:"fakeDns"` + LogConfig json_util.RawMessage `json:"log"` + RouterConfig json_util.RawMessage `json:"routing"` + DNSConfig json_util.RawMessage `json:"dns"` + InboundConfigs []InboundConfig `json:"inbounds"` + OutboundConfigs json_util.RawMessage `json:"outbounds"` + Transport json_util.RawMessage `json:"transport"` + Policy json_util.RawMessage `json:"policy"` + API json_util.RawMessage `json:"api"` + Stats json_util.RawMessage `json:"stats"` + Reverse json_util.RawMessage `json:"reverse"` + FakeDNS json_util.RawMessage `json:"fakeDns"` } -func (c *Config) MarshalJSON() ([]byte, error) { - return json_util.MarshalJSON(c) +func (c *Config) Equals(other *Config) bool { + if len(c.InboundConfigs) != len(other.InboundConfigs) { + return false + } + for i, inbound := range c.InboundConfigs { + if !inbound.Equals(&other.InboundConfigs[i]) { + return false + } + } + if !bytes.Equal(c.LogConfig, other.LogConfig) { + return false + } + if !bytes.Equal(c.RouterConfig, other.RouterConfig) { + return false + } + if !bytes.Equal(c.DNSConfig, other.DNSConfig) { + return false + } + if !bytes.Equal(c.OutboundConfigs, other.OutboundConfigs) { + return false + } + if !bytes.Equal(c.Transport, other.Transport) { + return false + } + if !bytes.Equal(c.Policy, other.Policy) { + return false + } + if !bytes.Equal(c.API, other.API) { + return false + } + if !bytes.Equal(c.Stats, other.Stats) { + return false + } + if !bytes.Equal(c.Reverse, other.Reverse) { + return false + } + if !bytes.Equal(c.FakeDNS, other.FakeDNS) { + return false + } + return true } diff --git a/xray/inbound.go b/xray/inbound.go index f22d7f1..461c2ee 100644 --- a/xray/inbound.go +++ b/xray/inbound.go @@ -1,20 +1,41 @@ package xray import ( - "encoding/json" + "bytes" "x-ui/util/json_util" ) type InboundConfig struct { - Listen json.RawMessage `json:"listen"` // listen 不能为空字符串 - Port int `json:"port"` - Protocol string `json:"protocol"` - Settings json.RawMessage `json:"settings"` - StreamSettings json.RawMessage `json:"streamSettings"` - Tag string `json:"tag"` - Sniffing json.RawMessage `json:"sniffing"` + Listen json_util.RawMessage `json:"listen"` // listen 不能为空字符串 + Port int `json:"port"` + Protocol string `json:"protocol"` + Settings json_util.RawMessage `json:"settings"` + StreamSettings json_util.RawMessage `json:"streamSettings"` + Tag string `json:"tag"` + Sniffing json_util.RawMessage `json:"sniffing"` } -func (i *InboundConfig) MarshalJSON() ([]byte, error) { - return json_util.MarshalJSON(i) +func (c *InboundConfig) Equals(other *InboundConfig) bool { + if !bytes.Equal(c.Listen, other.Listen) { + return false + } + if c.Port != other.Port { + return false + } + if c.Protocol != other.Protocol { + return false + } + if !bytes.Equal(c.Settings, other.Settings) { + return false + } + if !bytes.Equal(c.StreamSettings, other.StreamSettings) { + return false + } + if c.Tag != other.Tag { + return false + } + if !bytes.Equal(c.Sniffing, other.Sniffing) { + return false + } + return true } diff --git a/xray/process.go b/xray/process.go index 24d547f..ca91f58 100644 --- a/xray/process.go +++ b/xray/process.go @@ -62,16 +62,16 @@ type process struct { version string apiPort int - xrayConfig *Config - lines *queue.Queue - exitErr error + config *Config + lines *queue.Queue + exitErr error } -func newProcess(xrayConfig *Config) *process { +func newProcess(config *Config) *process { return &process{ - version: "Unknown", - xrayConfig: xrayConfig, - lines: queue.New(100), + version: "Unknown", + config: config, + lines: queue.New(100), } } @@ -90,6 +90,9 @@ func (p *process) GetErr() error { } func (p *process) GetResult() string { + if p.lines.Empty() && p.exitErr != nil { + return p.exitErr.Error() + } items, _ := p.lines.TakeUntil(func(item interface{}) bool { return true }) @@ -108,8 +111,12 @@ func (p *Process) GetAPIPort() int { return p.apiPort } +func (p *Process) GetConfig() *Config { + return p.config +} + func (p *process) refreshAPIPort() { - for _, inbound := range p.xrayConfig.InboundConfigs { + for _, inbound := range p.config.InboundConfigs { if inbound.Tag == "api" { p.apiPort = inbound.Port break @@ -132,19 +139,25 @@ func (p *process) refreshVersion() { } } -func (p *process) Start() error { +func (p *process) Start() (err error) { if p.IsRunning() { return errors.New("xray is already running") } - data, err := json.MarshalIndent(p.xrayConfig, "", " ") + defer func() { + if err != nil { + p.exitErr = err + } + }() + + data, err := json.MarshalIndent(p.config, "", " ") if err != nil { - return err + return common.NewErrorf("生成 xray 配置文件失败: %v", err) } configPath := GetConfigPath() err = os.WriteFile(configPath, data, fs.ModePerm) if err != nil { - return err + return common.NewErrorf("写入配置文件失败: %v", err) } cmd := exec.Command(GetBinaryPath(), "-c", configPath)