- 优化 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 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 面板"

12
main.go
View File

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

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 });
}
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:

View File

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

View File

@@ -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: {

View File

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

View File

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

View File

@@ -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()

View File

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

View File

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