瀏覽代碼

0.0.2

 - 增加设置总流量功能,流量超出后自动禁用
 - 优化部分 ui 细节
 - 修复监听 ip 不为空导致无法启动 xray 的问题
 - 修复二维码链接没有包含 address 的问题
sprov 4 年之前
父節點
當前提交
214b217f12
共有 14 個文件被更改,包括 149 次插入36 次删除
  1. 37 1
      README.md
  2. 0 0
      a.s
  3. 10 2
      config/config.go
  4. 1 0
      config/name
  5. 1 0
      config/version
  6. 9 3
      database/model/model.go
  7. 2 5
      main.go
  8. 9 0
      web/assets/js/model/models.js
  9. 1 1
      web/assets/js/util/utils.js
  10. 1 2
      web/global/global.go
  11. 12 0
      web/html/xui/form/inbound.html
  12. 39 16
      web/html/xui/inbounds.html
  13. 13 0
      web/service/inbound.go
  14. 14 6
      web/web.go

+ 37 - 1
README.md

@@ -1,2 +1,38 @@
 # x-ui
-a web panel based on xray-core
+支持多协议多用户 xray 面板
+
+# 功能介绍
+- 系统状态监控
+- 支持多用户多协议,网页可视化操作
+- 支持的协议:vmess、vless、trojan、shadowsocks、dokodemo-door、socks、http
+- 支持配置更多传输配置
+- 账号流量统计
+- 可自定义 xray 配置模板
+- 支持 https 访问面板(自备域名 + ssl 证书)
+- 更多高级配置项,详见面板
+
+# 安装&升级
+## 测试版
+```
+bash <(curl -Ls https://raw.githubusercontent.com/sprov065/x-ui/master/install.sh) 0.0.1
+```
+
+## 建议系统
+- CentOS 7+
+- Ubuntu 16+
+- Debian 8+
+
+# 常见问题
+## 与 v2-ui 关系
+x-ui 相当于 v2-ui 的加强版,未来会加入更多功能,待 x-ui 功能稳定后,v2-ui 将不再提供更新
+
+x-ui 可与 v2-ui 并存,数据不互通,不影响对方的运行
+
+# Telegram
+群组:https://t.me/sprov_blog
+
+频道:https://t.me/sprov_channel
+
+## Stargazers over time
+
+[![Stargazers over time](https://starchart.cc/sprov065/x-ui.svg)](https://starchart.cc/sprov065/x-ui)

+ 0 - 0
a.s


+ 10 - 2
config/config.go

@@ -1,10 +1,18 @@
 package config
 
 import (
+	_ "embed"
 	"fmt"
 	"os"
+	"strings"
 )
 
+//go:embed version
+var version string
+
+//go:embed name
+var name string
+
 type LogLevel string
 
 const (
@@ -15,11 +23,11 @@ const (
 )
 
 func GetVersion() string {
-	return "0.0.1"
+	return strings.TrimSpace(version)
 }
 
 func GetName() string {
-	return "x-ui"
+	return strings.TrimSpace(name)
 }
 
 func GetLogLevel() LogLevel {

+ 1 - 0
config/name

@@ -0,0 +1 @@
+x-ui

+ 1 - 0
config/version

@@ -0,0 +1 @@
+0.0.2

+ 9 - 3
database/model/model.go

@@ -1,6 +1,7 @@
 package model
 
 import (
+	"fmt"
 	"x-ui/util/json_util"
 	"x-ui/xray"
 )
@@ -25,8 +26,9 @@ type User struct {
 type Inbound struct {
 	Id         int    `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
 	UserId     int    `json:"-"`
-	Up         int64  `json:"up"`
-	Down       int64  `json:"down"`
+	Up         int64  `json:"up" form:"up"`
+	Down       int64  `json:"down" form:"down"`
+	Total      int64  `json:"total" form:"total"`
 	Remark     string `json:"remark" form:"remark"`
 	Enable     bool   `json:"enable" form:"enable"`
 	ExpiryTime int64  `json:"expiryTime" form:"expiryTime"`
@@ -42,8 +44,12 @@ type Inbound struct {
 }
 
 func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
+	listen := i.Listen
+	if listen != "" {
+		listen = fmt.Sprintf("\"%v\"", listen)
+	}
 	return &xray.InboundConfig{
-		Listen:         json_util.RawMessage(i.Listen),
+		Listen:         json_util.RawMessage(listen),
 		Port:           i.Port,
 		Protocol:       string(i.Protocol),
 		Settings:       json_util.RawMessage(i.Settings),

+ 2 - 5
main.go

@@ -18,9 +18,6 @@ import (
 	"x-ui/web/service"
 )
 
-// this function call global.setWebServer
-func setWebServer(server global.WebServer)
-
 func runWebServer() {
 	log.Printf("%v %v", config.GetName(), config.GetVersion())
 
@@ -45,7 +42,7 @@ func runWebServer() {
 	var server *web.Server
 
 	server = web.NewServer()
-	setWebServer(server)
+	global.SetWebServer(server)
 	err = server.Start()
 	if err != nil {
 		log.Println(err)
@@ -60,7 +57,7 @@ func runWebServer() {
 		if sig == syscall.SIGHUP {
 			server.Stop()
 			server = web.NewServer()
-			setWebServer(server)
+			global.SetWebServer(server)
 			err = server.Start()
 			if err != nil {
 				log.Println(err)

+ 9 - 0
web/assets/js/model/models.js

@@ -26,6 +26,7 @@ class DBInbound {
     userId = 0;
     up = 0;
     down = 0;
+    total = 0;
     remark = "";
     enable = true;
     expiryTime = 0;
@@ -45,6 +46,14 @@ class DBInbound {
         ObjectUtil.cloneProps(this, data);
     }
 
+    get totalGB() {
+        return toFixed(this.total / ONE_GB, 2);
+    }
+
+    set totalGB(gb) {
+        this.total = toFixed(gb * ONE_GB, 0);
+    }
+
     toInbound() {
         let settings = {};
         if (!ObjectUtil.isEmpty(this.settings)) {

+ 1 - 1
web/assets/js/util/utils.js

@@ -284,7 +284,7 @@ class ObjectUtil {
                 return false;
             }
         }
-        return true
+        return true;
     }
 
 }

+ 1 - 2
web/global/global.go

@@ -13,8 +13,7 @@ type WebServer interface {
 	GetCtx() context.Context
 }
 
-//go:linkname setWebServer main.setWebServer
-func setWebServer(s WebServer) {
+func SetWebServer(s WebServer) {
 	webServer = s
 }
 

+ 12 - 0
web/html/xui/form/inbound.html

@@ -27,6 +27,18 @@
     <a-form-item label="端口">
         <a-input type="number" v-model.number="inbound.port"></a-input>
     </a-form-item>
+    <a-form-item>
+        <span slot="label">
+            总流量(GB)
+            <a-tooltip>
+                <template slot="title">
+                    0 表示不限制
+                </template>
+                <a-icon type="question-circle" theme="filled"></a-icon>
+            </a-tooltip>
+        </span>
+        <a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
+    </a-form-item>
 </a-form>
 
 <!-- vmess settings -->

+ 39 - 16
web/html/xui/inbounds.html

@@ -27,15 +27,15 @@
                     <a-card hoverable style="margin-bottom: 20px;">
                         <a-row>
                             <a-col :xs="24" :sm="24" :lg="12">
-                                upload / download
+                                总上传 / 下载
                                 <a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
                             </a-col>
                             <a-col :xs="24" :sm="24" :lg="12">
-                                total traffic
+                                总用量
                                 <a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
                             </a-col>
                             <a-col :xs="24" :sm="24" :lg="12">
-                                number of accounts
+                                入站数量
                                 <a-tag color="green">[[ dbInbounds.length ]]</a-tag>
                             </a-col>
                         </a-row>
@@ -59,6 +59,8 @@
                             <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>
+                                <a-tag v-else color="cyan">无限制</a-tag>
                             </template>
                             <template slot="settings" slot-scope="text, dbInbound">
                                 <a-button type="link">查看</a-button>
@@ -76,6 +78,7 @@
                             <template slot="action" slot-scope="text, dbInbound">
                                 <a-button v-if="dbInbound.hasLink()" type="primary" icon="qrcode" @click="showQrcode(dbInbound)"></a-button>
                                 <a-button type="primary" icon="edit" @click="openEditInbound(dbInbound)"></a-button>
+                                <a-button icon="retweet" @click="resetTraffic(dbInbound)"></a-button>
                                 <a-button type="danger" icon="delete" @click="delInbound(dbInbound)"></a-button>
                             </template>
                         </a-table>
@@ -94,17 +97,17 @@
         dataIndex: "id",
         width: 60,
     }, {
-        title: "protocol",
+        title: "协议",
         align: 'center',
         width: 60,
         scopedSlots: { customRender: 'protocol' },
     }, {
-        title: "port",
+        title: "端口",
         align: 'center',
         dataIndex: "port",
         width: 60,
     }, {
-        title: "traffic",
+        title: "流量↑|↓",
         align: 'center',
         width: 60,
         scopedSlots: { customRender: 'traffic' },
@@ -119,7 +122,7 @@
     //     width: 60,
     //     scopedSlots: { customRender: 'streamSettings' },
     }, {
-        title: "enable",
+        title: "启用",
         align: 'center',
         width: 60,
         scopedSlots: { customRender: 'enable' },
@@ -172,8 +175,8 @@
             },
             openAddInbound() {
                 inModal.show({
-                    title: 'add account',
-                    okText: 'add',
+                    title: '添加入站',
+                    okText: '添加',
                     confirm: async (inbound, dbInbound) => {
                         inModal.loading();
                         await this.addInbound(inbound, dbInbound);
@@ -184,8 +187,8 @@
             openEditInbound(dbInbound) {
                 const inbound = dbInbound.toInbound();
                 inModal.show({
-                    title: 'update account',
-                    okText: 'update',
+                    title: '修改入站',
+                    okText: '修改',
                     inbound: inbound,
                     dbInbound: dbInbound,
                     confirm: async (inbound, dbInbound) => {
@@ -197,6 +200,9 @@
             },
             async addInbound(inbound, dbInbound) {
                 const data = {
+                    up: dbInbound.up,
+                    down: dbInbound.down,
+                    total: dbInbound.total,
                     remark: dbInbound.remark,
                     enable: dbInbound.enable,
 
@@ -211,6 +217,9 @@
             },
             async updateInbound(inbound, dbInbound) {
                 const data = {
+                    up: dbInbound.up,
+                    down: dbInbound.down,
+                    total: dbInbound.total,
                     remark: dbInbound.remark,
                     enable: dbInbound.enable,
 
@@ -223,18 +232,32 @@
                 };
                 await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
             },
+            resetTraffic(dbInbound) {
+                this.$confirm({
+                    title: '重置流量',
+                    content: '确定要重置流量吗?',
+                    okText: '重置',
+                    cancelText: '取消',
+                    onOk: () => {
+                        const inbound = dbInbound.toInbound();
+                        dbInbound.up = 0;
+                        dbInbound.down = 0;
+                        this.updateInbound(inbound, dbInbound);
+                    },
+                });
+            },
             delInbound(dbInbound) {
                 this.$confirm({
-                    title: 'delete account',
-                    content: 'Cannot be restored after deletion, confirm deletion?',
-                    okText: 'delete',
-                    cancelText: 'cancel',
+                    title: '删除入站',
+                    content: '确定要删除入站吗?',
+                    okText: '删除',
+                    cancelText: '取消',
                     onOk: () => this.submit('/xui/inbound/del/' + dbInbound.id),
                 });
             },
             showQrcode(dbInbound) {
                 let address = location.hostname;
-                if (!ObjectUtil.isEmpty(dbInbound.listen) || dbInbound.listen !== "0.0.0.0") {
+                if (!ObjectUtil.isEmpty(dbInbound.listen) && dbInbound.listen !== "0.0.0.0") {
                     address = dbInbound.listen;
                 }
                 const link = dbInbound.genLink(address);

+ 13 - 0
web/service/inbound.go

@@ -56,6 +56,9 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) error {
 	if err != nil {
 		return err
 	}
+	oldInbound.Up = inbound.Up
+	oldInbound.Down = inbound.Down
+	oldInbound.Total = inbound.Total
 	oldInbound.Remark = inbound.Remark
 	oldInbound.Enable = inbound.Enable
 	oldInbound.ExpiryTime = inbound.ExpiryTime
@@ -98,3 +101,13 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
 	}
 	return
 }
+
+func (s *InboundService) DisableInvalidInbounds() (bool, 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
+}

+ 14 - 6
web/web.go

@@ -21,7 +21,6 @@ import (
 	"time"
 	"x-ui/config"
 	"x-ui/logger"
-	"x-ui/util"
 	"x-ui/util/common"
 	"x-ui/web/controller"
 	"x-ui/web/service"
@@ -244,6 +243,7 @@ func (s *Server) startTask() {
 		logger.Warning("start xray failed:", err)
 	}
 	var checkTime = 0
+	// 每 30 秒检查一次 xray 是否在运行
 	s.cron.AddFunc("@every 30s", func() {
 		if s.xrayService.IsXrayRunning() {
 			checkTime = 0
@@ -255,9 +255,10 @@ func (s *Server) startTask() {
 		}
 		s.xrayService.SetIsNeedRestart(true)
 	})
+
 	go func() {
 		time.Sleep(time.Second * 5)
-		// 与重启 xray 的时间错开
+		// 每 10 秒统计一次流量,首次启动延迟 5 秒,与重启 xray 的时间错开
 		s.cron.AddFunc("@every 10s", func() {
 			if !s.xrayService.IsXrayRunning() {
 				return
@@ -273,6 +274,16 @@ func (s *Server) startTask() {
 			}
 		})
 	}()
+
+	// 每分钟检查一次 inbound 流量超出情况
+	s.cron.AddFunc("@every 1m", func() {
+		needRestart, err := s.inboundService.DisableInvalidInbounds()
+		if err != nil {
+			logger.Warning("disable invalid inbounds err:", err)
+		} else if needRestart {
+			s.xrayService.SetIsNeedRestart(true)
+		}
+	})
 }
 
 func (s *Server) Start() (err error) {
@@ -343,11 +354,8 @@ func (s *Server) Start() (err error) {
 }
 
 func (s *Server) Stop() error {
-	if util.IsDone(s.ctx) {
-		// 防止 gc 后调用第二次 Stop
-		s.xrayService.StopXray()
-	}
 	s.cancel()
+	s.xrayService.StopXray()
 	if s.cron != nil {
 		s.cron.Stop()
 	}