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/component/setting.html b/web/html/xui/component/setting.html
index 2b6f6a4..7b8b263 100644
--- a/web/html/xui/component/setting.html
+++ b/web/html/xui/component/setting.html
@@ -12,7 +12,7 @@
-
+
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)