浏览代码

0.1.0

 - 改进 ui 界面
 - 修复流量超出后账号不自动失效问题
 - 修复vless生成的链接不正确问题
 - 修复网页端重启面板功能问题
sprov 4 年之前
父节点
当前提交
e91daabb18

+ 1 - 1
config/version

@@ -1 +1 @@
-0.0.2
+0.1.0

+ 4 - 1
main.go

@@ -55,7 +55,10 @@ func runWebServer() {
 		sig := <-sigCh
 
 		if sig == syscall.SIGHUP {
-			server.Stop()
+			err := server.Stop()
+			if err != nil {
+				logger.Warning("stop server err:", err)
+			}
 			server = web.NewServer()
 			global.SetWebServer(server)
 			err = server.Start()

+ 34 - 2
web/assets/js/model/models.js

@@ -54,6 +54,38 @@ class DBInbound {
         this.total = toFixed(gb * ONE_GB, 0);
     }
 
+    get isVMess() {
+        return this.protocol === Protocols.VMESS;
+    }
+
+    get isVLess() {
+        return this.protocol === Protocols.VLESS;
+    }
+
+    get isTrojan() {
+        return this.protocol === Protocols.TROJAN;
+    }
+
+    get isSS() {
+        return this.protocol === Protocols.SHADOWSOCKS;
+    }
+
+    get isSocks() {
+        return this.protocol === Protocols.SOCKS;
+    }
+
+    get isHTTP() {
+        return this.protocol === Protocols.HTTP;
+    }
+
+    get address() {
+        let address = location.hostname;
+        if (!ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0") {
+            address = this.listen;
+        }
+        return address;
+    }
+
     toInbound() {
         let settings = {};
         if (!ObjectUtil.isEmpty(this.settings)) {
@@ -93,9 +125,9 @@ class DBInbound {
         }
     }
 
-    genLink(address = "") {
+    genLink() {
         const inbound = this.toInbound();
-        return inbound.genLink(address, this.remark);
+        return inbound.genLink(this.address, this.remark);
     }
 }
 

+ 77 - 2
web/assets/js/model/xray.js

@@ -643,6 +643,81 @@ class Inbound extends XrayCommonClass {
         this.stream.network = network;
     }
 
+    // VMess & VLess
+    get uuid() {
+        switch (this.protocol) {
+            case Protocols.VMESS:
+                return this.settings.vmesses[0].id;
+            case Protocols.VLESS:
+                return this.settings.vlesses[0].id;
+            default:
+                return "";
+        }
+    }
+
+    // VLess
+    get flow() {
+        switch (this.protocol) {
+            case Protocols.VLESS:
+                return this.settings.vlesses[0].flow;
+            default:
+                return "";
+        }
+    }
+
+    // VMess
+    get alterId() {
+        switch (this.protocol) {
+            case Protocols.VMESS:
+                return this.settings.vmesses[0].alterId;
+            default:
+                return "";
+        }
+    }
+
+    // Socks & HTTP
+    get username() {
+        switch (this.protocol) {
+            case Protocols.SOCKS:
+            case Protocols.HTTP:
+                return this.settings.accounts[0].user;
+            default:
+                return "";
+        }
+    }
+
+    // Trojan & Shadowsocks & Socks & HTTP
+    get password() {
+        switch (this.protocol) {
+            case Protocols.TROJAN:
+                return this.settings.clients[0].password;
+            case Protocols.SHADOWSOCKS:
+                return this.settings.password;
+            case Protocols.SOCKS:
+            case Protocols.HTTP:
+                return this.settings.accounts[0].pass;
+            default:
+                return "";
+        }
+    }
+
+    // Shadowsocks
+    get method() {
+        switch (this.protocol) {
+            case Protocols.SHADOWSOCKS:
+                return this.settings.method;
+            default:
+                return "";
+        }
+    }
+
+    get serverName() {
+        if (this.stream.isTls || this.stream.isXTls) {
+            return this.stream.tls.server;
+        }
+        return "";
+    }
+
     canEnableTls() {
         switch (this.protocol) {
             case Protocols.VMESS:
@@ -785,7 +860,7 @@ class Inbound extends XrayCommonClass {
         const type = this.stream.network;
         const params = new Map();
         params.set("type", this.stream.network);
-        if (this.isXTls) {
+        if (this.xtls) {
             params.set("security", "xtls");
         } else {
             params.set("security", this.stream.security);
@@ -841,7 +916,7 @@ class Inbound extends XrayCommonClass {
             }
         }
 
-        if (this.isXTls) {
+        if (this.xtls) {
             params.set("flow", this.settings.vlesses[0].flow);
         }
 

+ 4 - 5
web/controller/inbound.go

@@ -36,8 +36,7 @@ func (a *InboundController) startTask() {
 	webServer := global.GetWebServer()
 	c := webServer.GetCron()
 	c.AddFunc("@every 10s", func() {
-		if a.xrayService.IsNeedRestart() {
-			a.xrayService.SetIsNeedRestart(false)
+		if a.xrayService.IsNeedRestartAndSetFalse() {
 			err := a.xrayService.RestartXray()
 			if err != nil {
 				logger.Error("restart xray failed:", err)
@@ -70,7 +69,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
 	err = a.inboundService.AddInbound(inbound)
 	jsonMsg(c, "添加", err)
 	if err == nil {
-		a.xrayService.SetIsNeedRestart(true)
+		a.xrayService.SetToNeedRestart()
 	}
 }
 
@@ -83,7 +82,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
 	err = a.inboundService.DelInbound(id)
 	jsonMsg(c, "删除", err)
 	if err == nil {
-		a.xrayService.SetIsNeedRestart(true)
+		a.xrayService.SetToNeedRestart()
 	}
 }
 
@@ -104,6 +103,6 @@ func (a *InboundController) updateInbound(c *gin.Context) {
 	err = a.inboundService.UpdateInbound(inbound)
 	jsonMsg(c, "修改", err)
 	if err == nil {
-		a.xrayService.SetIsNeedRestart(true)
+		a.xrayService.SetToNeedRestart()
 	}
 }

+ 71 - 0
web/html/xui/component/inbound_info.html

@@ -0,0 +1,71 @@
+{{define "inboundInfoStream"}}
+<p>传输: <a-tag color="green">[[ inbound.network ]]</a-tag></p>
+
+<!-- TODO -->
+
+<template v-if="inbound.tls || inbound.xtls">
+    <p v-if="inbound.tls">tls: <a-tag color="green">开启</a-tag></p>
+    <p v-if="inbound.xtls">xtls: <a-tag color="green">开启</a-tag></p>
+</template>
+<template v-else>
+    <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>
+</p>
+<p v-if="inbound.xtls">
+    xtls域名: <a-tag color="green">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
+</p>
+{{end}}
+
+
+{{define "component/inboundInfoComponent"}}
+<div>
+    <p>协议: <a-tag color="green">[[ dbInbound.protocol ]]</a-tag></p>
+    <p>地址: <a-tag color="blue">[[ dbInbound.address ]]</a-tag></p>
+    <p>端口: <a-tag color="green">[[ dbInbound.port ]]</a-tag></p>
+
+    <template v-if="dbInbound.isVMess">
+        <p>uuid: <a-tag color="green">[[ inbound.uuid ]]</a-tag></p>
+        <p>alterId: <a-tag color="green">[[ inbound.alterId ]]</a-tag></p>
+    </template>
+
+    <template v-if="dbInbound.isVLess">
+        <p>uuid: <a-tag color="green">[[ inbound.uuid ]]</a-tag></p>
+        <p v-if="inbound.isXTls">flow: <a-tag color="green">[[ inbound.flow ]]</a-tag></p>
+    </template>
+
+    <template v-if="dbInbound.isTrojan">
+        <p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
+    </template>
+
+    <template v-if="dbInbound.isSS">
+        <p>加密: <a-tag color="green">[[ inbound.method ]]</a-tag></p>
+        <p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
+    </template>
+
+    <template v-if="dbInbound.isSocks">
+        <p>用户名: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
+        <p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
+    </template>
+
+    <template v-if="dbInbound.isHTTP">
+        <p>用户名: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
+        <p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
+    </template>
+
+    <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
+        {{template "inboundInfoStream"}}
+    </template>
+</div>
+{{end}}
+
+{{define "component/inboundInfo"}}
+<script>
+    Vue.component('inbound-info', {
+        delimiters: ['[[', ']]'],
+        props: ["dbInbound", "inbound"],
+        template: `{{template "component/inboundInfoComponent"}}`,
+    });
+</script>
+{{end}}

+ 1 - 1
web/html/xui/form/protocol/vless.html

@@ -3,7 +3,7 @@
     <a-form-item label="id">
         <a-input v-model.trim="inbound.settings.vlesses[0].id"></a-input>
     </a-form-item>
-    <a-form-item label="flow">
+    <a-form-item v-if="inbound.xtls" label="flow">
         <a-select v-model="inbound.settings.vlesses[0].flow" style="width: 150px">
             <a-select-option value="">无</a-select-option>
             <a-select-option v-for="key in VLESS_FLOW" :value="key">[[ key ]]</a-select-option>

+ 42 - 0
web/html/xui/inbound_info_modal.html

@@ -0,0 +1,42 @@
+{{define "inboundInfoModal"}}
+{{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" }}'>
+    <inbound-info :db-inbound="dbInbound" :inbound="inbound"></inbound-info>
+</a-modal>
+<script>
+
+    const infoModal = {
+        visible: false,
+        inbound: new Inbound(),
+        dbInbound: new DBInbound(),
+        ok() {
+
+        },
+        show(dbInbound) {
+            this.inbound = dbInbound.toInbound();
+            this.dbInbound = new DBInbound(dbInbound);
+            this.visible = true;
+        },
+        close() {
+            infoModal.visible = false;
+        },
+    };
+
+    new Vue({
+        delimiters: ['[[', ']]'],
+        el: '#inbound-info-modal',
+        data: {
+            infoModal,
+            get dbInbound() {
+                return this.infoModal.dbInbound;
+            },
+            get inbound() {
+                return this.infoModal.inbound;
+            }
+        },
+    });
+
+</script>
+{{end}}

+ 30 - 21
web/html/xui/inbounds.html

@@ -63,10 +63,15 @@
                                 <a-tag v-else color="cyan">无限制</a-tag>
                             </template>
                             <template slot="settings" slot-scope="text, dbInbound">
-                                <a-button type="link">查看</a-button>
+                                <a-button type="link" @click="showInfo(dbInbound)">查看</a-button>
                             </template>
-                            <template slot="streamSettings" slot-scope="text, dbInbound">
-                                <a-button type="link">查看</a-button>
+                            <template slot="stream" slot-scope="text, dbInbound, index">
+                                <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
+                                    <a-tag color="green">[[ inbounds[index].stream.network ]]</a-tag>
+                                    <a-tag v-if="inbounds[index].stream.isTls" color="blue">tls</a-tag>
+                                    <a-tag v-if="inbounds[index].stream.isXTls" color="blue">xtls</a-tag>
+                                </template>
+                                <template v-else>无</template>
                             </template>
                             <template slot="enable" slot-scope="text, dbInbound">
                                 <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></a-switch>
@@ -109,25 +114,25 @@
     }, {
         title: "流量↑|↓",
         align: 'center',
-        width: 60,
+        width: 80,
         scopedSlots: { customRender: 'traffic' },
-    // }, {
-    //     title: "settings",
-    //     align: 'center',
-    //     width: 60,
-    //     scopedSlots: { customRender: 'settings' },
-    // }, {
-    //     title: "streamSettings",
-    //     align: 'center',
-    //     width: 60,
-    //     scopedSlots: { customRender: 'streamSettings' },
+    }, {
+        title: "详细信息",
+        align: 'center',
+        width: 60,
+        scopedSlots: { customRender: 'settings' },
+    }, {
+        title: "传输配置",
+        align: 'center',
+        width: 60,
+        scopedSlots: { customRender: 'stream' },
     }, {
         title: "启用",
         align: 'center',
         width: 60,
         scopedSlots: { customRender: 'enable' },
     }, {
-        title: "action",
+        title: "操作",
         align: 'center',
         width: 60,
         scopedSlots: { customRender: 'action' },
@@ -139,6 +144,7 @@
         data: {
             siderDrawer,
             spinning: false,
+            inbounds: [],
             dbInbounds: [],
             searchKey: '',
         },
@@ -156,9 +162,12 @@
                 this.setInbounds(msg.obj);
             },
             setInbounds(dbInbounds) {
+                this.inbounds.splice(0);
                 this.dbInbounds.splice(0);
                 for (const inbound of dbInbounds) {
-                    this.dbInbounds.push(new DBInbound(inbound));
+                    const dbInbound = new DBInbound(inbound);
+                    this.inbounds.push(dbInbound.toInbound());
+                    this.dbInbounds.push(dbInbound);
                 }
             },
             searchInbounds(key) {
@@ -256,13 +265,12 @@
                 });
             },
             showQrcode(dbInbound) {
-                let address = location.hostname;
-                if (!ObjectUtil.isEmpty(dbInbound.listen) && dbInbound.listen !== "0.0.0.0") {
-                    address = dbInbound.listen;
-                }
-                const link = dbInbound.genLink(address);
+                const link = dbInbound.genLink();
                 qrModal.show('二维码', link);
             },
+            showInfo(dbInbound) {
+                infoModal.show(dbInbound);
+            },
             switchEnable(dbInbound) {
                 this.submit(`/xui/inbound/update/${dbInbound.id}`, dbInbound);
             },
@@ -301,5 +309,6 @@
 {{template "promptModal"}}
 {{template "qrcodeModal"}}
 {{template "textModal"}}
+{{template "inboundInfoModal"}}
 </body>
 </html>

+ 7 - 7
web/html/xui/setting.html

@@ -38,11 +38,11 @@
                     <a-tabs default-active-key="1">
                         <a-tab-pane key="1" tab="面板配置">
                             <a-list item-layout="horizontal" style="background: white">
-                                <setting-list-item type="text" title="面板监听 IP" desc="默认留空监听所有 IP" v-model="allSetting.webListen"></setting-list-item>
-                                <setting-list-item type="number" title="面板监听端口" v-model.number="allSetting.webPort"></setting-list-item>
-                                <setting-list-item type="text" title="面板证书公钥文件路径" desc="填写一个 '/' 开头的绝对路径" v-model="allSetting.webCertFile"></setting-list-item>
-                                <setting-list-item type="text" title="面板证书密钥文件路径" desc="填写一个 '/' 开头的绝对路径" v-model="allSetting.webKeyFile"></setting-list-item>
-                                <setting-list-item type="text" title="面板 url 根路径" desc="必须以 '/' 开头,以 '/' 结尾" v-model="allSetting.webBasePath"></setting-list-item>
+                                <setting-list-item type="text" title="面板监听 IP" desc="默认留空监听所有 IP,重启面板生效" v-model="allSetting.webListen"></setting-list-item>
+                                <setting-list-item type="number" title="面板监听端口" desc="重启面板生效" v-model.number="allSetting.webPort"></setting-list-item>
+                                <setting-list-item type="text" title="面板证书公钥文件路径" desc="填写一个 '/' 开头的绝对路径,重启面板生效" v-model="allSetting.webCertFile"></setting-list-item>
+                                <setting-list-item type="text" title="面板证书密钥文件路径" desc="填写一个 '/' 开头的绝对路径,重启面板生效" v-model="allSetting.webKeyFile"></setting-list-item>
+                                <setting-list-item type="text" title="面板 url 根路径" desc="必须以 '/' 开头,以 '/' 结尾,重启面板生效" v-model="allSetting.webBasePath"></setting-list-item>
                             </a-list>
                         </a-tab-pane>
                         <a-tab-pane key="2" tab="用户设置">
@@ -68,12 +68,12 @@
                         </a-tab-pane>
                         <a-tab-pane key="3" tab="xray 相关设置">
                             <a-list item-layout="horizontal" style="background: white">
-                                <setting-list-item type="textarea" title="xray 配置模版" desc="以该模版为基础生成最终的 xray 配置文件" v-model="allSetting.xrayTemplateConfig"></setting-list-item>
+                                <setting-list-item type="textarea" title="xray 配置模版" desc="以该模版为基础生成最终的 xray 配置文件,重启面板生效" v-model="allSetting.xrayTemplateConfig"></setting-list-item>
                             </a-list>
                         </a-tab-pane>
                         <a-tab-pane key="4" tab="其他设置">
                             <a-list item-layout="horizontal" style="background: white">
-                                <setting-list-item type="text" title="时区" desc="定时任务按照该时区的时间运行" v-model="allSetting.timeLocation"></setting-list-item>
+                                <setting-list-item type="text" title="时区" desc="定时任务按照该时区的时间运行,重启面板生效" v-model="allSetting.timeLocation"></setting-list-item>
                             </a-list>
                         </a-tab-pane>
                     </a-tabs>

+ 2 - 2
web/service/inbound.go

@@ -102,12 +102,12 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
 	return
 }
 
-func (s *InboundService) DisableInvalidInbounds() (bool, error) {
+func (s *InboundService) DisableInvalidInbounds() (int64, error) {
 	db := database.GetDB()
 	result := db.Model(model.Inbound{}).
 		Where("up + down >= total and total > 0 and enable = ?", true).
 		Update("enable", false)
 	err := result.Error
 	count := result.RowsAffected
-	return count > 0, err
+	return count, err
 }

+ 8 - 6
web/service/xray.go

@@ -5,18 +5,18 @@ import (
 	"errors"
 	"go.uber.org/atomic"
 	"sync"
+	"x-ui/logger"
 	"x-ui/xray"
 )
 
 var p *xray.Process
 var lock sync.Mutex
+var isNeedXrayRestart atomic.Bool
 var result string
 
 type XrayService struct {
 	inboundService InboundService
 	settingService SettingService
-
-	isNeedXrayRestart atomic.Bool
 }
 
 func (s *XrayService) IsXrayRunning() bool {
@@ -87,6 +87,7 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, error) {
 func (s *XrayService) RestartXray() error {
 	lock.Lock()
 	defer lock.Unlock()
+	logger.Debug("restart xray")
 
 	xrayConfig, err := s.GetXrayConfig()
 	if err != nil {
@@ -108,16 +109,17 @@ func (s *XrayService) RestartXray() error {
 func (s *XrayService) StopXray() error {
 	lock.Lock()
 	defer lock.Unlock()
+	logger.Debug("stop xray")
 	if s.IsXrayRunning() {
 		return p.Stop()
 	}
 	return errors.New("xray is not running")
 }
 
-func (s *XrayService) SetIsNeedRestart(needRestart bool) {
-	s.isNeedXrayRestart.Store(needRestart)
+func (s *XrayService) SetToNeedRestart() {
+	isNeedXrayRestart.Store(true)
 }
 
-func (s *XrayService) IsNeedRestart() bool {
-	return s.isNeedXrayRestart.Load()
+func (s *XrayService) IsNeedRestartAndSetFalse() bool {
+	return isNeedXrayRestart.CAS(true, false)
 }

+ 21 - 10
web/web.go

@@ -44,7 +44,8 @@ func (f *wrapAssetsFS) Open(name string) (fs.File, error) {
 }
 
 type Server struct {
-	listener net.Listener
+	httpServer *http.Server
+	listener   net.Listener
 
 	index  *controller.IndexController
 	server *controller.ServerController
@@ -253,7 +254,7 @@ func (s *Server) startTask() {
 		if checkTime < 2 {
 			return
 		}
-		s.xrayService.SetIsNeedRestart(true)
+		s.xrayService.SetToNeedRestart()
 	})
 
 	go func() {
@@ -275,13 +276,14 @@ func (s *Server) startTask() {
 		})
 	}()
 
-	// 每分钟检查一次 inbound 流量超出情况
-	s.cron.AddFunc("@every 1m", func() {
-		needRestart, err := s.inboundService.DisableInvalidInbounds()
+	// 每 30 秒检查一次 inbound 流量超出情况
+	s.cron.AddFunc("@every 30s", func() {
+		count, err := s.inboundService.DisableInvalidInbounds()
 		if err != nil {
 			logger.Warning("disable invalid inbounds err:", err)
-		} else if needRestart {
-			s.xrayService.SetIsNeedRestart(true)
+		} else if count > 0 {
+			logger.Debugf("disabled %v inbounds", count)
+			s.xrayService.SetToNeedRestart()
 		}
 	})
 }
@@ -348,7 +350,11 @@ func (s *Server) Start() (err error) {
 
 	s.startTask()
 
-	go engine.RunListener(listener)
+	s.httpServer = &http.Server{
+		Handler: engine,
+	}
+
+	go s.httpServer.Serve(listener)
 
 	return nil
 }
@@ -359,10 +365,15 @@ func (s *Server) Stop() error {
 	if s.cron != nil {
 		s.cron.Stop()
 	}
+	var err1 error
+	var err2 error
+	if s.httpServer != nil {
+		err1 = s.httpServer.Shutdown(s.ctx)
+	}
 	if s.listener != nil {
-		return s.listener.Close()
+		err2 = s.listener.Close()
 	}
-	return nil
+	return common.Combine(err1, err2)
 }
 
 func (s *Server) GetCtx() context.Context {