7 Commits
0.0.2 ... 0.2.0

Author SHA1 Message Date
sprov
f0356ad786 Update README.md 2021-06-17 11:11:56 +08:00
sprov
89677c4fe1 0.2.0
- 优化 ui 界面
 - 优化网站加载速度
 - 新增从 v2-ui 迁移账号数据的功能
2021-06-17 11:05:43 +08:00
sprov
d67dff5a4c Update README.md 2021-06-15 11:11:32 +08:00
sprov
e91daabb18 0.1.0
- 改进 ui 界面
 - 修复流量超出后账号不自动失效问题
 - 修复vless生成的链接不正确问题
 - 修复网页端重启面板功能问题
2021-06-15 11:10:39 +08:00
sprov
5cc4cf02ee Update README.md 2021-06-12 11:49:05 +08:00
sprov
1444a00630 Update README.md 2021-06-12 11:44:10 +08:00
sprov
ae4ea3e091 Update README.md 2021-06-12 11:39:34 +08:00
21 changed files with 695 additions and 71 deletions

View File

@@ -1,5 +1,5 @@
# x-ui # x-ui
支持多协议多用户 xray 面板 支持多协议多用户 xray 面板
# 功能介绍 # 功能介绍
- 系统状态监控 - 系统状态监控
@@ -14,7 +14,7 @@
# 安装&升级 # 安装&升级
## 测试版 ## 测试版
``` ```
bash <(curl -Ls https://raw.githubusercontent.com/sprov065/x-ui/master/install.sh) 0.0.1 bash <(curl -Ls https://raw.githubusercontent.com/sprov065/x-ui/master/install.sh) 0.1.0
``` ```
## 建议系统 ## 建议系统
@@ -28,6 +28,13 @@ x-ui 相当于 v2-ui 的加强版,未来会加入更多功能,待 x-ui 功
x-ui 可与 v2-ui 并存,数据不互通,不影响对方的运行 x-ui 可与 v2-ui 并存,数据不互通,不影响对方的运行
## 从 v2-ui 迁移
首先在安装了 v2-ui 的服务器上安装最新版 x-ui然后使用以下命令进行迁移将迁移本机 v2-ui 的`所有 inbound 账号数据`至 x-ui`面板设置和用户名密码不会迁移`
> 迁移成功后请`关闭 v2-ui` 并且`重启 x-ui`,否则 v2-ui 的 inbound 会与 x-ui 的 inbound 会产生`端口冲突`
```
x-ui v2-ui
```
# Telegram # Telegram
群组https://t.me/sprov_blog 群组https://t.me/sprov_blog

View File

@@ -1 +1 @@
0.0.2 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 面板"

17
main.go
View File

@@ -50,12 +50,16 @@ 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 {
server.Stop() case syscall.SIGHUP:
err := server.Stop()
if err != nil {
logger.Warning("stop server err:", err)
}
server = web.NewServer() server = web.NewServer()
global.SetWebServer(server) global.SetWebServer(server)
err = server.Start() err = server.Start()
@@ -63,8 +67,9 @@ func runWebServer() {
log.Println(err) log.Println(err)
return return
} }
} else { default:
continue server.Stop()
return
} }
} }
} }
@@ -170,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

@@ -54,6 +54,38 @@ class DBInbound {
this.total = toFixed(gb * ONE_GB, 0); 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() { toInbound() {
let settings = {}; let settings = {};
if (!ObjectUtil.isEmpty(this.settings)) { if (!ObjectUtil.isEmpty(this.settings)) {
@@ -93,9 +125,9 @@ class DBInbound {
} }
} }
genLink(address = "") { genLink() {
const inbound = this.toInbound(); const inbound = this.toInbound();
return inbound.genLink(address, this.remark); return inbound.genLink(this.address, this.remark);
} }
} }

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,151 @@ 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
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 "";
}
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:
@@ -785,7 +948,7 @@ class Inbound extends XrayCommonClass {
const type = this.stream.network; const type = this.stream.network;
const params = new Map(); const params = new Map();
params.set("type", this.stream.network); params.set("type", this.stream.network);
if (this.isXTls) { if (this.xtls) {
params.set("security", "xtls"); params.set("security", "xtls");
} else { } else {
params.set("security", this.stream.security); params.set("security", this.stream.security);
@@ -841,7 +1004,7 @@ class Inbound extends XrayCommonClass {
} }
} }
if (this.isXTls) { if (this.xtls) {
params.set("flow", this.settings.vlesses[0].flow); params.set("flow", this.settings.vlesses[0].flow);
} }

View File

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

View File

@@ -0,0 +1,92 @@
{{define "inboundInfoStream"}}
<p>传输: <a-tag color="green">[[ inbound.network ]]</a-tag></p>
<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>
<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="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
</p>
<p v-if="inbound.xtls">
xtls域名: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ 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}}

View File

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

View File

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

View File

@@ -59,14 +59,22 @@
<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">
<a-button type="link">查看</a-button> <a-button type="link" @click="showInfo(dbInbound)">查看</a-button>
</template> </template>
<template slot="streamSettings" slot-scope="text, dbInbound"> <template slot="stream" slot-scope="text, dbInbound, index">
<a-button type="link">查看</a-button> <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>
<template slot="enable" slot-scope="text, dbInbound"> <template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></a-switch> <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></a-switch>
@@ -96,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',
@@ -109,25 +122,25 @@
}, { }, {
title: "流量↑|↓", title: "流量↑|↓",
align: 'center', align: 'center',
width: 60, width: 80,
scopedSlots: { customRender: 'traffic' }, scopedSlots: { customRender: 'traffic' },
// }, { }, {
// title: "settings", title: "详细信息",
// align: 'center', align: 'center',
// width: 60, width: 60,
// scopedSlots: { customRender: 'settings' }, scopedSlots: { customRender: 'settings' },
// }, { }, {
// title: "streamSettings", title: "传输配置",
// align: 'center', align: 'center',
// width: 60, width: 60,
// scopedSlots: { customRender: 'streamSettings' }, scopedSlots: { customRender: 'stream' },
}, { }, {
title: "启用", title: "启用",
align: 'center', align: 'center',
width: 60, width: 60,
scopedSlots: { customRender: 'enable' }, scopedSlots: { customRender: 'enable' },
}, { }, {
title: "action", title: "操作",
align: 'center', align: 'center',
width: 60, width: 60,
scopedSlots: { customRender: 'action' }, scopedSlots: { customRender: 'action' },
@@ -139,6 +152,7 @@
data: { data: {
siderDrawer, siderDrawer,
spinning: false, spinning: false,
inbounds: [],
dbInbounds: [], dbInbounds: [],
searchKey: '', searchKey: '',
}, },
@@ -156,9 +170,12 @@
this.setInbounds(msg.obj); this.setInbounds(msg.obj);
}, },
setInbounds(dbInbounds) { setInbounds(dbInbounds) {
this.inbounds.splice(0);
this.dbInbounds.splice(0); this.dbInbounds.splice(0);
for (const inbound of dbInbounds) { 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) { searchInbounds(key) {
@@ -256,13 +273,12 @@
}); });
}, },
showQrcode(dbInbound) { showQrcode(dbInbound) {
let address = location.hostname; const link = dbInbound.genLink();
if (!ObjectUtil.isEmpty(dbInbound.listen) && dbInbound.listen !== "0.0.0.0") {
address = dbInbound.listen;
}
const link = dbInbound.genLink(address);
qrModal.show('二维码', link); qrModal.show('二维码', link);
}, },
showInfo(dbInbound) {
infoModal.show(dbInbound);
},
switchEnable(dbInbound) { switchEnable(dbInbound) {
this.submit(`/xui/inbound/update/${dbInbound.id}`, dbInbound); this.submit(`/xui/inbound/update/${dbInbound.id}`, dbInbound);
}, },
@@ -301,5 +317,6 @@
{{template "promptModal"}} {{template "promptModal"}}
{{template "qrcodeModal"}} {{template "qrcodeModal"}}
{{template "textModal"}} {{template "textModal"}}
{{template "inboundInfoModal"}}
</body> </body>
</html> </html>

View File

@@ -38,11 +38,11 @@
<a-tabs default-active-key="1"> <a-tabs default-active-key="1">
<a-tab-pane key="1" tab="面板配置"> <a-tab-pane key="1" tab="面板配置">
<a-list item-layout="horizontal" style="background: white"> <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="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="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.webCertFile"></setting-list-item>
<setting-list-item type="text" title="面板证书密钥文件路径" desc="填写一个 '/' 开头的绝对路径" v-model="allSetting.webKeyFile"></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="面板 url 根路径" desc="必须以 '/' 开头,以 '/' 结尾,重启面板生效" v-model="allSetting.webBasePath"></setting-list-item>
</a-list> </a-list>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="2" tab="用户设置"> <a-tab-pane key="2" tab="用户设置">
@@ -68,12 +68,12 @@
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="3" tab="xray 相关设置"> <a-tab-pane key="3" tab="xray 相关设置">
<a-list item-layout="horizontal" style="background: white"> <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-list>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="4" tab="其他设置"> <a-tab-pane key="4" tab="其他设置">
<a-list item-layout="horizontal" style="background: white"> <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-list>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>

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

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

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,16 +36,47 @@ 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 {
listener net.Listener httpServer *http.Server
listener net.Listener
index *controller.IndexController index *controller.IndexController
server *controller.ServerController server *controller.ServerController
@@ -130,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
@@ -253,7 +292,7 @@ func (s *Server) startTask() {
if checkTime < 2 { if checkTime < 2 {
return return
} }
s.xrayService.SetIsNeedRestart(true) s.xrayService.SetToNeedRestart()
}) })
go func() { go func() {
@@ -275,13 +314,14 @@ func (s *Server) startTask() {
}) })
}() }()
// 每分钟检查一次 inbound 流量超出情况 // 每 30 秒检查一次 inbound 流量超出情况
s.cron.AddFunc("@every 1m", func() { s.cron.AddFunc("@every 30s", func() {
needRestart, err := s.inboundService.DisableInvalidInbounds() count, err := s.inboundService.DisableInvalidInbounds()
if err != nil { if err != nil {
logger.Warning("disable invalid inbounds err:", err) logger.Warning("disable invalid inbounds err:", err)
} else if needRestart { } else if count > 0 {
s.xrayService.SetIsNeedRestart(true) logger.Debugf("disabled %v inbounds", count)
s.xrayService.SetToNeedRestart()
} }
}) })
} }
@@ -348,7 +388,11 @@ func (s *Server) Start() (err error) {
s.startTask() s.startTask()
go engine.RunListener(listener) s.httpServer = &http.Server{
Handler: engine,
}
go s.httpServer.Serve(listener)
return nil return nil
} }
@@ -359,10 +403,15 @@ func (s *Server) Stop() error {
if s.cron != nil { if s.cron != nil {
s.cron.Stop() s.cron.Stop()
} }
if s.listener != nil { var err1 error
return s.listener.Close() var err2 error
if s.httpServer != nil {
err1 = s.httpServer.Shutdown(s.ctx)
} }
return nil if s.listener != nil {
err2 = s.listener.Close()
}
return common.Combine(err1, err2)
} }
func (s *Server) GetCtx() context.Context { func (s *Server) GetCtx() context.Context {

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