- 优化 ui 界面
 - 优化网站加载速度
 - 新增从 v2-ui 迁移账号数据的功能
This commit is contained in:
sprov
2021-06-17 11:05:43 +08:00
parent d67dff5a4c
commit 89677c4fe1
15 changed files with 391 additions and 18 deletions

View File

@@ -1 +1 @@
0.1.0 0.2.0

View File

@@ -137,6 +137,7 @@ install_x-ui() {
echo -e "x-ui enable - 设置 x-ui 开机自启" echo -e "x-ui enable - 设置 x-ui 开机自启"
echo -e "x-ui disable - 取消 x-ui 开机自启" echo -e "x-ui disable - 取消 x-ui 开机自启"
echo -e "x-ui log - 查看 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 update - 更新 x-ui 面板"
echo -e "x-ui install - 安装 x-ui 面板" echo -e "x-ui install - 安装 x-ui 面板"
echo -e "x-ui uninstall - 卸载 x-ui 面板" echo -e "x-ui uninstall - 卸载 x-ui 面板"

12
main.go
View File

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

28
v2ui/db.go Normal file
View File

@@ -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
v2ui/models.go Normal file
View File

@@ -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,
}
}

View File

@@ -1,7 +1,51 @@
package v2ui 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 { 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
} }

File diff suppressed because one or more lines are too long

View File

@@ -168,6 +168,15 @@ TcpStreamSettings.TcpRequest = class extends XrayCommonClass {
this.headers.push({ name: name, value: value }); 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) { removeHeader(index) {
this.headers.splice(index, 1); this.headers.splice(index, 1);
} }
@@ -294,6 +303,15 @@ class WsStreamSettings extends XrayCommonClass {
this.headers.push({ name: name, value: value }); 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) { removeHeader(index) {
this.headers.splice(index, 1); this.headers.splice(index, 1);
} }
@@ -643,6 +661,30 @@ class Inbound extends XrayCommonClass {
this.stream.network = network; 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 // VMess & VLess
get uuid() { get uuid() {
switch (this.protocol) { switch (this.protocol) {
@@ -718,6 +760,52 @@ class Inbound extends XrayCommonClass {
return ""; 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() { canEnableTls() {
switch (this.protocol) { switch (this.protocol) {
case Protocols.VMESS: case Protocols.VMESS:

View File

@@ -1,7 +1,28 @@
{{define "inboundInfoStream"}} {{define "inboundInfoStream"}}
<p>传输: <a-tag color="green">[[ inbound.network ]]</a-tag></p> <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"> <template v-if="inbound.tls || inbound.xtls">
<p v-if="inbound.tls">tls: <a-tag color="green">开启</a-tag></p> <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> <p>tls: <a-tag color="red">关闭</a-tag></p>
</template> </template>
<p v-if="inbound.tls"> <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>
<p v-if="inbound.xtls"> <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> </p>
{{end}} {{end}}

View File

@@ -2,7 +2,7 @@
{{template "component/inboundInfo"}} {{template "component/inboundInfo"}}
<a-modal id="inbound-info-modal" v-model="infoModal.visible" title="详细信息" @ok="infoModal.ok" <a-modal id="inbound-info-modal" v-model="infoModal.visible" title="详细信息" @ok="infoModal.ok"
:closable="true" :mask-closable="true" :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> <inbound-info :db-inbound="dbInbound" :inbound="inbound"></inbound-info>
</a-modal> </a-modal>
<script> <script>
@@ -11,20 +11,39 @@
visible: false, visible: false,
inbound: new Inbound(), inbound: new Inbound(),
dbInbound: new DBInbound(), dbInbound: new DBInbound(),
ok() { clipboard: null,
okBtnPros: {
attrs: {
id: "inbound-info-modal-ok-btn",
style: "",
},
}, },
show(dbInbound) { show(dbInbound) {
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.dbInbound = new DBInbound(dbInbound); this.dbInbound = new DBInbound(dbInbound);
this.visible = true; 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() { close() {
infoModal.visible = false; infoModal.visible = false;
}, },
}; };
new Vue({ const infoModalApp = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#inbound-info-modal', el: '#inbound-info-modal',
data: { data: {

View File

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

View File

@@ -5,6 +5,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"x-ui/database" "x-ui/database"
"x-ui/database/model" "x-ui/database/model"
"x-ui/util/common"
"x-ui/xray" "x-ui/xray"
) )
@@ -31,11 +32,64 @@ func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
return inbounds, nil 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 { 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() db := database.GetDB()
return db.Save(inbound).Error 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 { func (s *InboundService) DelInbound(id int) error {
db := database.GetDB() db := database.GetDB()
return db.Delete(model.Inbound{}, id).Error 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 { 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) oldInbound, err := s.GetInbound(inbound.Id)
if err != nil { if err != nil {
return err return err

View File

@@ -11,6 +11,19 @@ import (
type UserService struct { 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 { func (s *UserService) CheckUser(username string, password string) *model.User {
db := database.GetDB() db := database.GetDB()

View File

@@ -18,6 +18,7 @@ import (
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"strings"
"time" "time"
"x-ui/config" "x-ui/config"
"x-ui/logger" "x-ui/logger"
@@ -35,12 +36,42 @@ var htmlFS embed.FS
//go:embed translation/* //go:embed translation/*
var i18nFS embed.FS var i18nFS embed.FS
var startTime = time.Now()
type wrapAssetsFS struct { type wrapAssetsFS struct {
embed.FS embed.FS
} }
func (f *wrapAssetsFS) Open(name string) (fs.File, error) { 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 { type Server struct {
@@ -131,12 +162,19 @@ func (s *Server) initRouter() (*gin.Engine, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
assetsBasePath := basePath + "assets/"
store := cookie.NewStore(secret) store := cookie.NewStore(secret)
engine.Use(sessions.Sessions("session", store)) engine.Use(sessions.Sessions("session", store))
engine.Use(func(c *gin.Context) { engine.Use(func(c *gin.Context) {
c.Set("base_path", basePath) 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) err = s.initI18n(engine)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -270,6 +270,12 @@ show_log() {
fi fi
} }
migrate_v2_ui() {
/usr/local/x-ui/x-ui v2-ui
before_show_menu
}
install_bbr() { install_bbr() {
bash <(curl -L -s https://raw.githubusercontent.com/sprov065/blog/master/bbr.sh) bash <(curl -L -s https://raw.githubusercontent.com/sprov065/blog/master/bbr.sh)
echo "" echo ""
@@ -393,6 +399,7 @@ show_usage() {
echo "x-ui enable - 设置 x-ui 开机自启" echo "x-ui enable - 设置 x-ui 开机自启"
echo "x-ui disable - 取消 x-ui 开机自启" echo "x-ui disable - 取消 x-ui 开机自启"
echo "x-ui log - 查看 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 update - 更新 x-ui 面板"
echo "x-ui install - 安装 x-ui 面板" echo "x-ui install - 安装 x-ui 面板"
echo "x-ui uninstall - 卸载 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 && show_log 0
;; ;;
"log") check_install 0 && migrate_v2_ui 0
;;
"update") check_install 0 && update 0 "update") check_install 0 && update 0
;; ;;
"install") check_uninstall 0 && install 0 "install") check_uninstall 0 && install 0