Browse Source

0.2.0

 - 优化 ui 界面
 - 优化网站加载速度
 - 新增从 v2-ui 迁移账号数据的功能
sprov 4 năm trước cách đây
mục cha
commit
89677c4fe1

+ 1 - 1
config/version

@@ -1 +1 @@
-0.1.0
+0.2.0

+ 1 - 0
install.sh

@@ -137,6 +137,7 @@ install_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 v2-ui        - 迁移本机器的 v2-ui 账号数据至 x-ui"
     echo -e "x-ui update       - 更新 x-ui 面板"
     echo -e "x-ui install      - 安装 x-ui 面板"
     echo -e "x-ui uninstall    - 卸载 x-ui 面板"

+ 7 - 5
main.go

@@ -50,11 +50,12 @@ func runWebServer() {
 	}
 
 	sigCh := make(chan os.Signal, 1)
-	signal.Notify(sigCh, syscall.SIGHUP)
+	signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
 	for {
 		sig := <-sigCh
 
-		if sig == syscall.SIGHUP {
+		switch sig {
+		case syscall.SIGHUP:
 			err := server.Stop()
 			if err != nil {
 				logger.Warning("stop server err:", err)
@@ -66,8 +67,9 @@ func runWebServer() {
 				log.Println(err)
 				return
 			}
-		} else {
-			continue
+		default:
+			server.Stop()
+			return
 		}
 	}
 }
@@ -173,7 +175,7 @@ func main() {
 		}
 		err = v2ui.MigrateFromV2UI(dbPath)
 		if err != nil {
-			logger.Error("migrate from v2-ui failed:", err)
+			fmt.Println("migrate from v2-ui failed:", err)
 		}
 	case "setting":
 		err := settingCmd.Parse(os.Args[2:])

+ 28 - 0
v2ui/db.go

@@ -0,0 +1,28 @@
+package v2ui
+
+import (
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+)
+
+var v2db *gorm.DB
+
+func initDB(dbPath string) error {
+	c := &gorm.Config{
+		Logger: logger.Discard,
+	}
+	var err error
+	v2db, err = gorm.Open(sqlite.Open(dbPath), c)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func getV2Inbounds() ([]*V2Inbound, error) {
+	inbounds := make([]*V2Inbound, 0)
+	err := v2db.Model(V2Inbound{}).Find(&inbounds).Error
+	return inbounds, err
+}

+ 41 - 0
v2ui/models.go

@@ -0,0 +1,41 @@
+package v2ui
+
+import "x-ui/database/model"
+
+type V2Inbound struct {
+	Id             int `gorm:"primaryKey;autoIncrement"`
+	Port           int `gorm:"unique"`
+	Listen         string
+	Protocol       string
+	Settings       string
+	StreamSettings string
+	Tag            string `gorm:"unique"`
+	Sniffing       string
+	Remark         string
+	Up             int64
+	Down           int64
+	Enable         bool
+}
+
+func (i *V2Inbound) TableName() string {
+	return "inbound"
+}
+
+func (i *V2Inbound) ToInbound(userId int) *model.Inbound {
+	return &model.Inbound{
+		UserId:         userId,
+		Up:             i.Up,
+		Down:           i.Down,
+		Total:          0,
+		Remark:         i.Remark,
+		Enable:         i.Enable,
+		ExpiryTime:     0,
+		Listen:         i.Listen,
+		Port:           i.Port,
+		Protocol:       model.Protocol(i.Protocol),
+		Settings:       i.Settings,
+		StreamSettings: i.StreamSettings,
+		Tag:            i.Tag,
+		Sniffing:       i.Sniffing,
+	}
+}

+ 46 - 2
v2ui/v2ui.go

@@ -1,7 +1,51 @@
 package v2ui
 
-import "errors"
+import (
+	"fmt"
+	"x-ui/config"
+	"x-ui/database"
+	"x-ui/database/model"
+	"x-ui/util/common"
+	"x-ui/web/service"
+)
 
 func MigrateFromV2UI(dbPath string) error {
-	return errors.New("not support right now")
+	err := initDB(dbPath)
+	if err != nil {
+		return common.NewError("init v2-ui database failed:", err)
+	}
+	err = database.InitDB(config.GetDBPath())
+	if err != nil {
+		return common.NewError("init x-ui database failed:", err)
+	}
+
+	v2Inbounds, err := getV2Inbounds()
+	if err != nil {
+		return common.NewError("get v2-ui inbounds failed:", err)
+	}
+	if len(v2Inbounds) == 0 {
+		fmt.Println("migrate v2-ui inbounds success: 0")
+		return nil
+	}
+
+	userService := service.UserService{}
+	user, err := userService.GetFirstUser()
+	if err != nil {
+		return common.NewError("get x-ui user failed:", err)
+	}
+
+	inbounds := make([]*model.Inbound, 0)
+	for _, v2inbound := range v2Inbounds {
+		inbounds = append(inbounds, v2inbound.ToInbound(user.Id))
+	}
+
+	inboundService := service.InboundService{}
+	err = inboundService.AddInbounds(inbounds)
+	if err != nil {
+		return common.NewError("add x-ui inbounds failed:", err)
+	}
+
+	fmt.Println("migrate v2-ui inbounds success:", len(inbounds))
+
+	return nil
 }

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
web/assets/[email protected]/antd.min.js


+ 88 - 0
web/assets/js/model/xray.js

@@ -168,6 +168,15 @@ TcpStreamSettings.TcpRequest = class extends XrayCommonClass {
         this.headers.push({ name: name, value: value });
     }
 
+    getHeader(name) {
+        for (const header of this.headers) {
+            if (header.name.toLowerCase() === name.toLowerCase()) {
+                return header.value;
+            }
+        }
+        return null;
+    }
+
     removeHeader(index) {
         this.headers.splice(index, 1);
     }
@@ -294,6 +303,15 @@ class WsStreamSettings extends XrayCommonClass {
         this.headers.push({ name: name, value: value });
     }
 
+    getHeader(name) {
+        for (const header of this.headers) {
+            if (header.name.toLowerCase() === name.toLowerCase()) {
+                return header.value;
+            }
+        }
+        return null;
+    }
+
     removeHeader(index) {
         this.headers.splice(index, 1);
     }
@@ -643,6 +661,30 @@ class Inbound extends XrayCommonClass {
         this.stream.network = network;
     }
 
+    get isTcp() {
+        return this.network === "tcp";
+    }
+
+    get isWs() {
+        return this.network === "ws";
+    }
+
+    get isKcp() {
+        return this.network === "kcp";
+    }
+
+    get isQuic() {
+        return this.network === "quic"
+    }
+
+    get isGrpc() {
+        return this.network === "grpc";
+    }
+
+    get isH2() {
+        return this.network === "http";
+    }
+
     // VMess & VLess
     get uuid() {
         switch (this.protocol) {
@@ -718,6 +760,52 @@ class Inbound extends XrayCommonClass {
         return "";
     }
 
+    get host() {
+        if (this.isTcp) {
+            return this.stream.tcp.request.getHeader("Host");
+        } else if (this.isWs) {
+            return this.stream.ws.getHeader("Host");
+        } else if (this.isH2) {
+            return this.stream.http.host[0];
+        }
+        return null;
+    }
+
+    get path() {
+        if (this.isTcp) {
+            return this.stream.tcp.request.path[0];
+        } else if (this.isWs) {
+            return this.stream.ws.path;
+        } else if (this.isH2) {
+            return this.stream.http.path[0];
+        }
+        return null;
+    }
+
+    get quicSecurity() {
+        return this.stream.quic.security;
+    }
+
+    get quicKey() {
+        return this.stream.quic.key;
+    }
+
+    get quicType() {
+        return this.stream.quic.type;
+    }
+
+    get kcpType() {
+        return this.stream.kcp.type;
+    }
+
+    get kcpSeed() {
+        return this.stream.kcp.seed;
+    }
+
+    get serviceName() {
+        return this.stream.grpc.serviceName;
+    }
+
     canEnableTls() {
         switch (this.protocol) {
             case Protocols.VMESS:

+ 24 - 3
web/html/xui/component/inbound_info.html

@@ -1,7 +1,28 @@
 {{define "inboundInfoStream"}}
 <p>传输: <a-tag color="green">[[ inbound.network ]]</a-tag></p>
 
-<!-- TODO -->
+<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
+    <p v-if="inbound.host">host: <a-tag color="green">[[ inbound.host ]]</a-tag></p>
+    <p v-else>host: <a-tag color="orange">无</a-tag></p>
+
+    <p v-if="inbound.path">path: <a-tag color="green">[[ inbound.path ]]</a-tag></p>
+    <p v-else>path: <a-tag color="orange">无</a-tag></p>
+</template>
+
+<template v-if="inbound.isQuic">
+    <p>quic 加密: <a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></p>
+    <p>quic 密码: <a-tag color="green">[[ inbound.quicKey ]]</a-tag></p>
+    <p>quic 伪装: <a-tag color="green">[[ inbound.quicType ]]</a-tag></p>
+</template>
+
+<template v-if="inbound.isKcp">
+    <p>kcp 加密: <a-tag color="green">[[ inbound.kcpType ]]</a-tag></p>
+    <p>kcp 密码: <a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></p>
+</template>
+
+<template v-if="inbound.isGrpc">
+    <p>grpc serviceName: <a-tag color="green">[[ inbound.serviceName ]]</a-tag></p>
+</template>
 
 <template v-if="inbound.tls || inbound.xtls">
     <p v-if="inbound.tls">tls: <a-tag color="green">开启</a-tag></p>
@@ -11,10 +32,10 @@
     <p>tls: <a-tag color="red">关闭</a-tag></p>
 </template>
 <p v-if="inbound.tls">
-    tls域名: <a-tag color="green">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
+    tls域名: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
 </p>
 <p v-if="inbound.xtls">
-    xtls域名: <a-tag color="green">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
+    xtls域名: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
 </p>
 {{end}}
 

+ 23 - 4
web/html/xui/inbound_info_modal.html

@@ -2,7 +2,7 @@
 {{template "component/inboundInfo"}}
 <a-modal id="inbound-info-modal" v-model="infoModal.visible" title="详细信息" @ok="infoModal.ok"
          :closable="true" :mask-closable="true"
-         ok-text="复制链接" cancel-text='{{ i18n "close" }}'>
+         ok-text="复制链接" cancel-text='{{ i18n "close" }}' :ok-button-props="infoModal.okBtnPros">
     <inbound-info :db-inbound="dbInbound" :inbound="inbound"></inbound-info>
 </a-modal>
 <script>
@@ -11,20 +11,39 @@
         visible: false,
         inbound: new Inbound(),
         dbInbound: new DBInbound(),
-        ok() {
-
+        clipboard: null,
+        okBtnPros: {
+            attrs: {
+                id: "inbound-info-modal-ok-btn",
+                style: "",
+            },
         },
         show(dbInbound) {
             this.inbound = dbInbound.toInbound();
             this.dbInbound = new DBInbound(dbInbound);
             this.visible = true;
+
+            if (dbInbound.hasLink()) {
+                this.okBtnPros.attrs.style = "";
+            } else {
+                this.okBtnPros.attrs.style = "display: none";
+            }
+
+            if (this.clipboard == null) {
+                infoModalApp.$nextTick(() => {
+                    this.clipboard = new ClipboardJS(`#${this.okBtnPros.attrs.id}`, {
+                        text: () => this.dbInbound.genLink(),
+                    });
+                    this.clipboard.on('success', () => app.$message.success('复制成功'));
+                });
+            }
         },
         close() {
             infoModal.visible = false;
         },
     };
 
-    new Vue({
+    const infoModalApp = new Vue({
         delimiters: ['[[', ']]'],
         el: '#inbound-info-modal',
         data: {

+ 9 - 1
web/html/xui/inbounds.html

@@ -59,7 +59,10 @@
                             <template slot="traffic" slot-scope="text, dbInbound">
                                 <a-tag color="blue">[[ sizeFormat(dbInbound.up) ]]</a-tag>
                                 <a-tag color="green">[[ sizeFormat(dbInbound.down) ]]</a-tag>
-                                <a-tag v-if="dbInbound.total > 0" color="cyan">[[ sizeFormat(dbInbound.total) ]]</a-tag>
+                                <template v-if="dbInbound.total > 0">
+                                    <a-tag v-if="dbInbound.up + dbInbound.down < dbInbound.total" color="cyan">[[ sizeFormat(dbInbound.total) ]]</a-tag>
+                                    <a-tag v-else color="red">[[ sizeFormat(dbInbound.total) ]]</a-tag>
+                                </template>
                                 <a-tag v-else color="cyan">无限制</a-tag>
                             </template>
                             <template slot="settings" slot-scope="text, dbInbound">
@@ -101,6 +104,11 @@
         align: 'center',
         dataIndex: "id",
         width: 60,
+    }, {
+        title: "备注",
+        align: 'center',
+        width: 60,
+        scopedSlots: { customRender: 'remark' },
     }, {
         title: "协议",
         align: 'center',

+ 62 - 0
web/service/inbound.go

@@ -5,6 +5,7 @@ import (
 	"gorm.io/gorm"
 	"x-ui/database"
 	"x-ui/database/model"
+	"x-ui/util/common"
 	"x-ui/xray"
 )
 
@@ -31,11 +32,64 @@ func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
 	return inbounds, nil
 }
 
+func (s *InboundService) checkPortExist(port int, ignoreId int) (bool, error) {
+	db := database.GetDB()
+	db = db.Model(model.Inbound{}).Where("port = ?", port)
+	if ignoreId > 0 {
+		db = db.Where("id != ?", ignoreId)
+	}
+	var count int64
+	err := db.Count(&count).Error
+	if err != nil {
+		return false, err
+	}
+	return count > 0, nil
+}
+
 func (s *InboundService) AddInbound(inbound *model.Inbound) error {
+	exist, err := s.checkPortExist(inbound.Port, 0)
+	if err != nil {
+		return err
+	}
+	if exist {
+		return common.NewError("端口已存在:", inbound.Port)
+	}
 	db := database.GetDB()
 	return db.Save(inbound).Error
 }
 
+func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
+	for _, inbound := range inbounds {
+		exist, err := s.checkPortExist(inbound.Port, 0)
+		if err != nil {
+			return err
+		}
+		if exist {
+			return common.NewError("端口已存在:", inbound.Port)
+		}
+	}
+
+	db := database.GetDB()
+	tx := db.Begin()
+	var err error
+	defer func() {
+		if err == nil {
+			tx.Commit()
+		} else {
+			tx.Rollback()
+		}
+	}()
+
+	for _, inbound := range inbounds {
+		err = tx.Save(inbound).Error
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 func (s *InboundService) DelInbound(id int) error {
 	db := database.GetDB()
 	return db.Delete(model.Inbound{}, id).Error
@@ -52,6 +106,14 @@ func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
 }
 
 func (s *InboundService) UpdateInbound(inbound *model.Inbound) error {
+	exist, err := s.checkPortExist(inbound.Port, inbound.Id)
+	if err != nil {
+		return err
+	}
+	if exist {
+		return common.NewError("端口已存在:", inbound.Port)
+	}
+
 	oldInbound, err := s.GetInbound(inbound.Id)
 	if err != nil {
 		return err

+ 13 - 0
web/service/user.go

@@ -11,6 +11,19 @@ import (
 type UserService struct {
 }
 
+func (s *UserService) GetFirstUser() (*model.User, error) {
+	db := database.GetDB()
+
+	user := &model.User{}
+	err := db.Model(model.User{}).
+		First(user).
+		Error
+	if err != nil {
+		return nil, err
+	}
+	return user, nil
+}
+
 func (s *UserService) CheckUser(username string, password string) *model.User {
 	db := database.GetDB()
 

+ 39 - 1
web/web.go

@@ -18,6 +18,7 @@ import (
 	"net/http"
 	"os"
 	"strconv"
+	"strings"
 	"time"
 	"x-ui/config"
 	"x-ui/logger"
@@ -35,12 +36,42 @@ var htmlFS embed.FS
 //go:embed translation/*
 var i18nFS embed.FS
 
+var startTime = time.Now()
+
 type wrapAssetsFS struct {
 	embed.FS
 }
 
 func (f *wrapAssetsFS) Open(name string) (fs.File, error) {
-	return f.FS.Open("assets/" + name)
+	file, err := f.FS.Open("assets/" + name)
+	if err != nil {
+		return nil, err
+	}
+	return &wrapAssetsFile{
+		File: file,
+	}, nil
+}
+
+type wrapAssetsFile struct {
+	fs.File
+}
+
+func (f *wrapAssetsFile) Stat() (fs.FileInfo, error) {
+	info, err := f.File.Stat()
+	if err != nil {
+		return nil, err
+	}
+	return &wrapAssetsFileInfo{
+		FileInfo: info,
+	}, nil
+}
+
+type wrapAssetsFileInfo struct {
+	fs.FileInfo
+}
+
+func (f *wrapAssetsFileInfo) ModTime() time.Time {
+	return startTime
 }
 
 type Server struct {
@@ -131,12 +162,19 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 	if err != nil {
 		return nil, err
 	}
+	assetsBasePath := basePath + "assets/"
 
 	store := cookie.NewStore(secret)
 	engine.Use(sessions.Sessions("session", store))
 	engine.Use(func(c *gin.Context) {
 		c.Set("base_path", basePath)
 	})
+	engine.Use(func(c *gin.Context) {
+		uri := c.Request.RequestURI
+		if strings.HasPrefix(uri, assetsBasePath) {
+			c.Header("Cache-Control", "max-age=31536000")
+		}
+	})
 	err = s.initI18n(engine)
 	if err != nil {
 		return nil, err

+ 9 - 0
x-ui.sh

@@ -270,6 +270,12 @@ show_log() {
     fi
 }
 
+migrate_v2_ui() {
+    /usr/local/x-ui/x-ui v2-ui
+
+    before_show_menu
+}
+
 install_bbr() {
     bash <(curl -L -s https://raw.githubusercontent.com/sprov065/blog/master/bbr.sh)
     echo ""
@@ -393,6 +399,7 @@ show_usage() {
     echo "x-ui enable       - 设置 x-ui 开机自启"
     echo "x-ui disable      - 取消 x-ui 开机自启"
     echo "x-ui log          - 查看 x-ui 日志"
+    echo "x-ui v2-ui        - 迁移本机器的 v2-ui 账号数据至 x-ui"
     echo "x-ui update       - 更新 x-ui 面板"
     echo "x-ui install      - 安装 x-ui 面板"
     echo "x-ui uninstall    - 卸载 x-ui 面板"
@@ -480,6 +487,8 @@ if [[ $# > 0 ]]; then
         ;;
         "log") check_install 0 && show_log 0
         ;;
+        "log") check_install 0 && migrate_v2_ui 0
+        ;;
         "update") check_install 0 && update 0
         ;;
         "install") check_uninstall 0 && install 0

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác