Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0356ad786 | ||
|
|
89677c4fe1 | ||
|
|
d67dff5a4c | ||
|
|
e91daabb18 | ||
|
|
5cc4cf02ee | ||
|
|
1444a00630 | ||
|
|
ae4ea3e091 | ||
|
|
214b217f12 | ||
|
|
f6eb413597 | ||
|
|
e5788c6d8e |
45
README.md
45
README.md
@@ -1,2 +1,45 @@
|
||||
# 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.1.0
|
||||
```
|
||||
|
||||
## 建议系统
|
||||
- CentOS 7+
|
||||
- Ubuntu 16+
|
||||
- Debian 8+
|
||||
|
||||
# 常见问题
|
||||
## 与 v2-ui 关系
|
||||
x-ui 相当于 v2-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
|
||||
群组:https://t.me/sprov_blog
|
||||
|
||||
频道:https://t.me/sprov_channel
|
||||
|
||||
## Stargazers over time
|
||||
|
||||
[](https://starchart.cc/sprov065/x-ui)
|
||||
|
||||
@@ -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
config/name
Normal file
1
config/name
Normal file
@@ -0,0 +1 @@
|
||||
x-ui
|
||||
1
config/version
Normal file
1
config/version
Normal file
@@ -0,0 +1 @@
|
||||
0.2.0
|
||||
@@ -26,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"`
|
||||
@@ -43,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(fmt.Sprintf("\"%s\"", i.Listen)),
|
||||
Listen: json_util.RawMessage(listen),
|
||||
Port: i.Port,
|
||||
Protocol: string(i.Protocol),
|
||||
Settings: json_util.RawMessage(i.Settings),
|
||||
|
||||
@@ -112,7 +112,7 @@ install_x-ui() {
|
||||
tar zxvf x-ui-linux-${arch}.tar.gz
|
||||
rm x-ui-linux-${arch}.tar.gz -f
|
||||
cd x-ui
|
||||
chmod +x x-ui bin/xray-x-ui-linux-${arch}
|
||||
chmod +x x-ui bin/xray-linux-${arch}
|
||||
cp -f x-ui.service /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
@@ -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 面板"
|
||||
|
||||
24
main.go
24
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)
|
||||
@@ -53,21 +50,26 @@ 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 {
|
||||
server.Stop()
|
||||
switch sig {
|
||||
case syscall.SIGHUP:
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
logger.Warning("stop server err:", err)
|
||||
}
|
||||
server = web.NewServer()
|
||||
setWebServer(server)
|
||||
global.SetWebServer(server)
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
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
28
v2ui/db.go
Normal 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
41
v2ui/models.go
Normal 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,
|
||||
}
|
||||
}
|
||||
48
v2ui/v2ui.go
48
v2ui/v2ui.go
@@ -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
|
||||
}
|
||||
|
||||
1
web/assets/ant-design-vue@1.7.2/antd.min.js
vendored
1
web/assets/ant-design-vue@1.7.2/antd.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -26,6 +26,7 @@ class DBInbound {
|
||||
userId = 0;
|
||||
up = 0;
|
||||
down = 0;
|
||||
total = 0;
|
||||
remark = "";
|
||||
enable = true;
|
||||
expiryTime = 0;
|
||||
@@ -45,6 +46,46 @@ 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);
|
||||
}
|
||||
|
||||
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)) {
|
||||
@@ -84,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,151 @@ 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) {
|
||||
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() {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
@@ -785,7 +948,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 +1004,7 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isXTls) {
|
||||
if (this.xtls) {
|
||||
params.set("flow", this.settings.vlesses[0].flow);
|
||||
}
|
||||
|
||||
|
||||
@@ -284,7 +284,7 @@ class ObjectUtil {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
92
web/html/xui/component/inbound_info.html
Normal file
92
web/html/xui/component/inbound_info.html
Normal 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}}
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
61
web/html/xui/inbound_info_modal.html
Normal file
61
web/html/xui/inbound_info_modal.html
Normal 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}}
|
||||
@@ -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,12 +59,22 @@
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -76,6 +86,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,37 +105,42 @@
|
||||
dataIndex: "id",
|
||||
width: 60,
|
||||
}, {
|
||||
title: "protocol",
|
||||
title: "备注",
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'remark' },
|
||||
}, {
|
||||
title: "协议",
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'protocol' },
|
||||
}, {
|
||||
title: "port",
|
||||
title: "端口",
|
||||
align: 'center',
|
||||
dataIndex: "port",
|
||||
width: 60,
|
||||
}, {
|
||||
title: "traffic",
|
||||
title: "流量↑|↓",
|
||||
align: 'center',
|
||||
width: 80,
|
||||
scopedSlots: { customRender: 'traffic' },
|
||||
}, {
|
||||
title: "详细信息",
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'traffic' },
|
||||
// }, {
|
||||
// title: "settings",
|
||||
// align: 'center',
|
||||
// width: 60,
|
||||
// scopedSlots: { customRender: 'settings' },
|
||||
// }, {
|
||||
// title: "streamSettings",
|
||||
// align: 'center',
|
||||
// width: 60,
|
||||
// scopedSlots: { customRender: 'streamSettings' },
|
||||
scopedSlots: { customRender: 'settings' },
|
||||
}, {
|
||||
title: "enable",
|
||||
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' },
|
||||
@@ -136,6 +152,7 @@
|
||||
data: {
|
||||
siderDrawer,
|
||||
spinning: false,
|
||||
inbounds: [],
|
||||
dbInbounds: [],
|
||||
searchKey: '',
|
||||
},
|
||||
@@ -153,9 +170,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) {
|
||||
@@ -172,8 +192,8 @@
|
||||
},
|
||||
openAddInbound() {
|
||||
inModal.show({
|
||||
title: 'add account',
|
||||
okText: 'add',
|
||||
title: '添加入站',
|
||||
okText: '添加',
|
||||
confirm: async (inbound, dbInbound) => {
|
||||
inModal.loading();
|
||||
await this.addInbound(inbound, dbInbound);
|
||||
@@ -184,8 +204,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 +217,9 @@
|
||||
},
|
||||
async addInbound(inbound, dbInbound) {
|
||||
const data = {
|
||||
up: dbInbound.up,
|
||||
down: dbInbound.down,
|
||||
total: dbInbound.total,
|
||||
remark: dbInbound.remark,
|
||||
enable: dbInbound.enable,
|
||||
|
||||
@@ -211,6 +234,9 @@
|
||||
},
|
||||
async updateInbound(inbound, dbInbound) {
|
||||
const data = {
|
||||
up: dbInbound.up,
|
||||
down: dbInbound.down,
|
||||
total: dbInbound.total,
|
||||
remark: dbInbound.remark,
|
||||
enable: dbInbound.enable,
|
||||
|
||||
@@ -223,23 +249,36 @@
|
||||
};
|
||||
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") {
|
||||
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);
|
||||
},
|
||||
@@ -278,5 +317,6 @@
|
||||
{{template "promptModal"}}
|
||||
{{template "qrcodeModal"}}
|
||||
{{template "textModal"}}
|
||||
{{template "inboundInfoModal"}}
|
||||
</body>
|
||||
</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>
|
||||
|
||||
@@ -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,10 +106,21 @@ 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
|
||||
}
|
||||
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 +163,13 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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, err
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
83
web/web.go
83
web/web.go
@@ -18,10 +18,10 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/controller"
|
||||
"x-ui/web/service"
|
||||
@@ -36,16 +36,47 @@ 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 {
|
||||
listener net.Listener
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
|
||||
index *controller.IndexController
|
||||
server *controller.ServerController
|
||||
@@ -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
|
||||
@@ -244,6 +282,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
|
||||
@@ -253,11 +292,12 @@ func (s *Server) startTask() {
|
||||
if checkTime < 2 {
|
||||
return
|
||||
}
|
||||
s.xrayService.SetIsNeedRestart(true)
|
||||
s.xrayService.SetToNeedRestart()
|
||||
})
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Second * 5)
|
||||
// 与重启 xray 的时间错开
|
||||
// 每 10 秒统计一次流量,首次启动延迟 5 秒,与重启 xray 的时间错开
|
||||
s.cron.AddFunc("@every 10s", func() {
|
||||
if !s.xrayService.IsXrayRunning() {
|
||||
return
|
||||
@@ -273,6 +313,17 @@ func (s *Server) startTask() {
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
// 每 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 count > 0 {
|
||||
logger.Debugf("disabled %v inbounds", count)
|
||||
s.xrayService.SetToNeedRestart()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) Start() (err error) {
|
||||
@@ -337,24 +388,30 @@ 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
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
if s.listener != nil {
|
||||
return s.listener.Close()
|
||||
var err1 error
|
||||
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 {
|
||||
|
||||
12
x-ui.service
Normal file
12
x-ui.service
Normal file
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=x-ui Service
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/usr/local/x-ui/
|
||||
ExecStart=/usr/local/x-ui/x-ui
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
x-ui.sh
9
x-ui.sh
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user