Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
292d5b89d4 | ||
|
|
f1057b1142 | ||
|
|
b8da50dde7 | ||
|
|
991e711ac1 | ||
|
|
1468984c92 | ||
|
|
e8b0d2c839 | ||
|
|
00f9de6f1c | ||
|
|
51f80b445d | ||
|
|
4bf10cd8b7 | ||
|
|
a02f03f5d6 | ||
|
|
7a660506ea | ||
|
|
f0356ad786 | ||
|
|
89677c4fe1 | ||
|
|
d67dff5a4c | ||
|
|
e91daabb18 | ||
|
|
5cc4cf02ee | ||
|
|
1444a00630 | ||
|
|
ae4ea3e091 |
14
.github/ISSUE_TEMPLATE/------.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/------.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
name: 提问题点这里
|
||||||
|
about: issue 模板
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
任何由于自己的配置错误导致的情况,请自行解决,issues 只用于解决面板自身的 bug
|
||||||
|
|
||||||
|
如果你确定面板的功能实现有 bug,请尽可能提供更多更精确的描述信息、复现方法与复现结果等等,而不是草草一句话了事,这对于问题的解决没有帮助
|
||||||
|
|
||||||
|
提问的艺术: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md
|
||||||
12
README.md
12
README.md
@@ -1,5 +1,5 @@
|
|||||||
# x-ui
|
# x-ui
|
||||||
支持多协议多用户 xray 面板
|
支持多协议多用户的 xray 面板
|
||||||
|
|
||||||
# 功能介绍
|
# 功能介绍
|
||||||
- 系统状态监控
|
- 系统状态监控
|
||||||
@@ -12,9 +12,8 @@
|
|||||||
- 更多高级配置项,详见面板
|
- 更多高级配置项,详见面板
|
||||||
|
|
||||||
# 安装&升级
|
# 安装&升级
|
||||||
## 测试版
|
|
||||||
```
|
```
|
||||||
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.2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
## 建议系统
|
## 建议系统
|
||||||
@@ -28,6 +27,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
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.0.2
|
0.3.0
|
||||||
@@ -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 面板"
|
||||||
|
|||||||
@@ -7,16 +7,22 @@ import (
|
|||||||
|
|
||||||
var logger *logging.Logger
|
var logger *logging.Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
InitLogger(logging.INFO)
|
||||||
|
}
|
||||||
|
|
||||||
func InitLogger(level logging.Level) {
|
func InitLogger(level logging.Level) {
|
||||||
format := logging.MustStringFormatter(
|
format := logging.MustStringFormatter(
|
||||||
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
|
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
|
||||||
)
|
)
|
||||||
logger = logging.MustGetLogger("x-ui")
|
newLogger := logging.MustGetLogger("x-ui")
|
||||||
backend := logging.NewLogBackend(os.Stderr, "", 0)
|
backend := logging.NewLogBackend(os.Stderr, "", 0)
|
||||||
backendFormatter := logging.NewBackendFormatter(backend, format)
|
backendFormatter := logging.NewBackendFormatter(backend, format)
|
||||||
backendLeveled := logging.AddModuleLevel(backendFormatter)
|
backendLeveled := logging.AddModuleLevel(backendFormatter)
|
||||||
backendLeveled.SetLevel(level, "")
|
backendLeveled.SetLevel(level, "")
|
||||||
logger.SetBackend(backendLeveled)
|
newLogger.SetBackend(backendLeveled)
|
||||||
|
|
||||||
|
logger = newLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
func Debug(args ...interface{}) {
|
func Debug(args ...interface{}) {
|
||||||
|
|||||||
17
main.go
17
main.go
@@ -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:])
|
||||||
|
|||||||
0
util/sys/a.s
Normal file
0
util/sys/a.s
Normal file
8
util/sys/psutil.go
Normal file
8
util/sys/psutil.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:linkname HostProc github.com/shirou/gopsutil/internal/common.HostProc
|
||||||
|
func HostProc(combineWith ...string) string
|
||||||
23
util/sys/sys_darwin.go
Normal file
23
util/sys/sys_darwin.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// +build darwin
|
||||||
|
|
||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/shirou/gopsutil/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetTCPCount() (int, error) {
|
||||||
|
stats, err := net.Connections("tcp")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(stats), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUDPCount() (int, error) {
|
||||||
|
stats, err := net.Connections("udp")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(stats), nil
|
||||||
|
}
|
||||||
70
util/sys/sys_linux.go
Normal file
70
util/sys/sys_linux.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
package sys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getLinesNum(filename string) (int, error) {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
sum := 0
|
||||||
|
buf := make([]byte, 8192)
|
||||||
|
for {
|
||||||
|
n, err := file.Read(buf)
|
||||||
|
|
||||||
|
var buffPosition int
|
||||||
|
for {
|
||||||
|
i := bytes.IndexByte(buf[buffPosition:], '\n')
|
||||||
|
if i < 0 || n == buffPosition {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buffPosition += i + 1
|
||||||
|
sum++
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
return sum, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return sum, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTCPCount() (int, error) {
|
||||||
|
root := HostProc()
|
||||||
|
|
||||||
|
tcp4, err := getLinesNum(fmt.Sprintf("%v/net/tcp", root))
|
||||||
|
if err != nil {
|
||||||
|
return tcp4, err
|
||||||
|
}
|
||||||
|
tcp6, err := getLinesNum(fmt.Sprintf("%v/net/tcp6", root))
|
||||||
|
if err != nil {
|
||||||
|
return tcp4 + tcp6, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return tcp4 + tcp6, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUDPCount() (int, error) {
|
||||||
|
root := HostProc()
|
||||||
|
|
||||||
|
udp4, err := getLinesNum(fmt.Sprintf("%v/net/udp", root))
|
||||||
|
if err != nil {
|
||||||
|
return udp4, err
|
||||||
|
}
|
||||||
|
udp6, err := getLinesNum(fmt.Sprintf("%v/net/udp6", root))
|
||||||
|
if err != nil {
|
||||||
|
return udp4 + udp6, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return udp4 + udp6, nil
|
||||||
|
}
|
||||||
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
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
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
@@ -1,14 +1,18 @@
|
|||||||
class User {
|
class User {
|
||||||
username = "";
|
|
||||||
password = "";
|
constructor() {
|
||||||
|
this.username = "";
|
||||||
|
this.password = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Msg {
|
class Msg {
|
||||||
success = false;
|
|
||||||
msg = "";
|
|
||||||
obj = null;
|
|
||||||
|
|
||||||
constructor(success, msg, obj) {
|
constructor(success, msg, obj) {
|
||||||
|
this.success = false;
|
||||||
|
this.msg = "";
|
||||||
|
this.obj = null;
|
||||||
|
|
||||||
if (success != null) {
|
if (success != null) {
|
||||||
this.success = success;
|
this.success = success;
|
||||||
}
|
}
|
||||||
@@ -22,24 +26,25 @@ class Msg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DBInbound {
|
class DBInbound {
|
||||||
id = 0;
|
|
||||||
userId = 0;
|
|
||||||
up = 0;
|
|
||||||
down = 0;
|
|
||||||
total = 0;
|
|
||||||
remark = "";
|
|
||||||
enable = true;
|
|
||||||
expiryTime = 0;
|
|
||||||
|
|
||||||
listen = "";
|
|
||||||
port = 0;
|
|
||||||
protocol = "";
|
|
||||||
settings = "";
|
|
||||||
streamSettings = "";
|
|
||||||
tag = "";
|
|
||||||
sniffing = "";
|
|
||||||
|
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
|
this.id = 0;
|
||||||
|
this.userId = 0;
|
||||||
|
this.up = 0;
|
||||||
|
this.down = 0;
|
||||||
|
this.total = 0;
|
||||||
|
this.remark = "";
|
||||||
|
this.enable = true;
|
||||||
|
this.expiryTime = 0;
|
||||||
|
|
||||||
|
this.listen = "";
|
||||||
|
this.port = 0;
|
||||||
|
this.protocol = "";
|
||||||
|
this.settings = "";
|
||||||
|
this.streamSettings = "";
|
||||||
|
this.tag = "";
|
||||||
|
this.sniffing = "";
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -54,6 +59,57 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
get _expiryTime() {
|
||||||
|
if (this.expiryTime === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return moment(this.expiryTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
set _expiryTime(t) {
|
||||||
|
if (t == null) {
|
||||||
|
this.expiryTime = 0;
|
||||||
|
} else {
|
||||||
|
this.expiryTime = t.valueOf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isExpiry() {
|
||||||
|
return this.expiryTime < new Date().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
toInbound() {
|
toInbound() {
|
||||||
let settings = {};
|
let settings = {};
|
||||||
if (!ObjectUtil.isEmpty(this.settings)) {
|
if (!ObjectUtil.isEmpty(this.settings)) {
|
||||||
@@ -93,24 +149,25 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AllSetting {
|
class AllSetting {
|
||||||
webListen = "";
|
|
||||||
webPort = 54321;
|
|
||||||
webCertFile = "";
|
|
||||||
webKeyFile = "";
|
|
||||||
webBasePath = "/";
|
|
||||||
|
|
||||||
xrayTemplateConfig = "";
|
|
||||||
|
|
||||||
timeLocation = "Asia/Shanghai";
|
|
||||||
|
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
|
this.webListen = "";
|
||||||
|
this.webPort = 54321;
|
||||||
|
this.webCertFile = "";
|
||||||
|
this.webKeyFile = "";
|
||||||
|
this.webBasePath = "/";
|
||||||
|
|
||||||
|
this.xrayTemplateConfig = "";
|
||||||
|
|
||||||
|
this.timeLocation = "Asia/Shanghai";
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1002,7 +1165,7 @@ Inbound.VmessSettings = class extends Inbound.Settings {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
|
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
|
||||||
constructor(id=RandomUtil.randomUUID(), alterId=64) {
|
constructor(id=RandomUtil.randomUUID(), alterId=0) {
|
||||||
super();
|
super();
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.alterId = alterId;
|
this.alterId = alterId;
|
||||||
|
|||||||
@@ -77,18 +77,19 @@ class PromiseUtil {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const seq = [
|
||||||
|
'a', 'b', 'c', 'd', 'e', 'f', 'g',
|
||||||
|
'h', 'i', 'j', 'k', 'l', 'm', 'n',
|
||||||
|
'o', 'p', 'q', 'r', 's', 't',
|
||||||
|
'u', 'v', 'w', 'x', 'y', 'z',
|
||||||
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||||
|
'A', 'B', 'C', 'D', 'E', 'F', 'G',
|
||||||
|
'H', 'I', 'J', 'K', 'L', 'M', 'N',
|
||||||
|
'O', 'P', 'Q', 'R', 'S', 'T',
|
||||||
|
'U', 'V', 'W', 'X', 'Y', 'Z'
|
||||||
|
];
|
||||||
|
|
||||||
class RandomUtil {
|
class RandomUtil {
|
||||||
static seq = [
|
|
||||||
'a', 'b', 'c', 'd', 'e', 'f', 'g',
|
|
||||||
'h', 'i', 'j', 'k', 'l', 'm', 'n',
|
|
||||||
'o', 'p', 'q', 'r', 's', 't',
|
|
||||||
'u', 'v', 'w', 'x', 'y', 'z',
|
|
||||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
|
||||||
'A', 'B', 'C', 'D', 'E', 'F', 'G',
|
|
||||||
'H', 'I', 'J', 'K', 'L', 'M', 'N',
|
|
||||||
'O', 'P', 'Q', 'R', 'S', 'T',
|
|
||||||
'U', 'V', 'W', 'X', 'Y', 'Z'
|
|
||||||
];
|
|
||||||
|
|
||||||
static randomIntRange(min, max) {
|
static randomIntRange(min, max) {
|
||||||
return parseInt(Math.random() * (max - min) + min, 10);
|
return parseInt(Math.random() * (max - min) + min, 10);
|
||||||
@@ -101,7 +102,7 @@ class RandomUtil {
|
|||||||
static randomSeq(count) {
|
static randomSeq(count) {
|
||||||
let str = '';
|
let str = '';
|
||||||
for (let i = 0; i < count; ++i) {
|
for (let i = 0; i < count; ++i) {
|
||||||
str += this.seq[this.randomInt(62)];
|
str += seq[this.randomInt(62)];
|
||||||
}
|
}
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
@@ -109,7 +110,7 @@ class RandomUtil {
|
|||||||
static randomLowerAndNum(count) {
|
static randomLowerAndNum(count) {
|
||||||
let str = '';
|
let str = '';
|
||||||
for (let i = 0; i < count; ++i) {
|
for (let i = 0; i < count; ++i) {
|
||||||
str += this.seq[this.randomInt(36)];
|
str += seq[this.randomInt(36)];
|
||||||
}
|
}
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
@@ -121,7 +122,7 @@ class RandomUtil {
|
|||||||
if (index <= 9) {
|
if (index <= 9) {
|
||||||
str += index;
|
str += index;
|
||||||
} else {
|
} else {
|
||||||
str += this.seq[index - 10];
|
str += seq[index - 10];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return str;
|
return str;
|
||||||
|
|||||||
@@ -36,9 +36,8 @@ 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(false)
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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}}
|
||||||
@@ -39,6 +39,19 @@
|
|||||||
</span>
|
</span>
|
||||||
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
|
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<span slot="label">
|
||||||
|
到期时间
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
留空则永不到期
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||||
|
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
||||||
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<!-- vmess settings -->
|
<!-- vmess settings -->
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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}}
|
||||||
@@ -46,40 +46,69 @@
|
|||||||
<div slot="title">
|
<div slot="title">
|
||||||
<a-button type="primary" icon="plus" @click="openAddInbound"></a-button>
|
<a-button type="primary" icon="plus" @click="openAddInbound"></a-button>
|
||||||
</div>
|
</div>
|
||||||
<a-input v-model="searchKey" placeholder="search" autofocus style="max-width: 300px"></a-input>
|
<!-- <a-input v-model="searchKey" placeholder="搜索" autofocus style="max-width: 300px"></a-input>-->
|
||||||
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
||||||
:data-source="dbInbounds"
|
:data-source="dbInbounds"
|
||||||
:loading="spinning" :scroll="{ x: 1500 }"
|
:loading="spinning" :scroll="{ x: 1500 }"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
style="margin-top: 20px"
|
style="margin-top: 20px"
|
||||||
@change="() => getDBInbounds()">
|
@change="() => getDBInbounds()">
|
||||||
|
<template slot="action" slot-scope="text, dbInbound">
|
||||||
|
<a-dropdown :trigger="['click']">
|
||||||
|
<a @click="e => e.preventDefault()">操作</a>
|
||||||
|
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)">
|
||||||
|
<a-menu-item v-if="dbInbound.hasLink()" key="qrcode">
|
||||||
|
<a-icon type="qrcode"></a-icon>二维码
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="edit">
|
||||||
|
<a-icon type="edit"></a-icon>编辑
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="resetTraffic">
|
||||||
|
<a-icon type="retweet"></a-icon>重置流量
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="delete">
|
||||||
|
<span style="color: #FF4D4F">
|
||||||
|
<a-icon type="delete"></a-icon>删除
|
||||||
|
</span>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</a-dropdown>
|
||||||
|
</template>
|
||||||
<template slot="protocol" slot-scope="text, dbInbound">
|
<template slot="protocol" slot-scope="text, dbInbound">
|
||||||
<a-tag color="blue">[[ dbInbound.protocol ]]</a-tag>
|
<a-tag color="blue">[[ dbInbound.protocol ]]</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<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) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
|
||||||
<a-tag color="green">[[ sizeFormat(dbInbound.down) ]]</a-tag>
|
<template v-if="dbInbound.total > 0">
|
||||||
<a-tag v-if="dbInbound.total > 0" color="cyan">[[ sizeFormat(dbInbound.total) ]]</a-tag>
|
<a-tag v-if="dbInbound.up + dbInbound.down < dbInbound.total" color="cyan">[[ sizeFormat(dbInbound.total) ]]</a-tag>
|
||||||
<a-tag v-else color="cyan">无限制</a-tag>
|
<a-tag v-else color="red">[[ sizeFormat(dbInbound.total) ]]</a-tag>
|
||||||
|
</template>
|
||||||
|
<a-tag v-else color="green">无限制</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>
|
||||||
</template>
|
</template>
|
||||||
<template slot="expiryTime" slot-scope="text, dbInbound">
|
<template slot="expiryTime" slot-scope="text, dbInbound">
|
||||||
<span v-if="dbInbound.expiryTime > 0" color="red">[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]</span>
|
<template v-if="dbInbound.expiryTime > 0">
|
||||||
<span v-else>无限期</span>
|
<a-tag v-if="dbInbound.isExpiry" color="red">
|
||||||
</template>
|
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
|
||||||
<template slot="action" slot-scope="text, dbInbound">
|
</a-tag>
|
||||||
<a-button v-if="dbInbound.hasLink()" type="primary" icon="qrcode" @click="showQrcode(dbInbound)"></a-button>
|
<a-tag v-else color="blue">
|
||||||
<a-button type="primary" icon="edit" @click="openEditInbound(dbInbound)"></a-button>
|
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
|
||||||
<a-button icon="retweet" @click="resetTraffic(dbInbound)"></a-button>
|
</a-tag>
|
||||||
<a-button type="danger" icon="delete" @click="delInbound(dbInbound)"></a-button>
|
</template>
|
||||||
|
<a-tag v-else color="green">无限期</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-card>
|
</a-card>
|
||||||
@@ -92,10 +121,25 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
const columns = [{
|
const columns = [{
|
||||||
|
title: "操作",
|
||||||
|
align: 'center',
|
||||||
|
width: 30,
|
||||||
|
scopedSlots: { customRender: 'action' },
|
||||||
|
}, {
|
||||||
|
title: "启用",
|
||||||
|
align: 'center',
|
||||||
|
width: 40,
|
||||||
|
scopedSlots: { customRender: 'enable' },
|
||||||
|
}, {
|
||||||
title: "id",
|
title: "id",
|
||||||
align: 'center',
|
align: 'center',
|
||||||
dataIndex: "id",
|
dataIndex: "id",
|
||||||
width: 60,
|
width: 30,
|
||||||
|
}, {
|
||||||
|
title: "备注",
|
||||||
|
align: 'center',
|
||||||
|
width: 100,
|
||||||
|
dataIndex: "remark",
|
||||||
}, {
|
}, {
|
||||||
title: "协议",
|
title: "协议",
|
||||||
align: 'center',
|
align: 'center',
|
||||||
@@ -109,28 +153,23 @@
|
|||||||
}, {
|
}, {
|
||||||
title: "流量↑|↓",
|
title: "流量↑|↓",
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 60,
|
width: 150,
|
||||||
scopedSlots: { customRender: 'traffic' },
|
scopedSlots: { customRender: 'traffic' },
|
||||||
// }, {
|
|
||||||
// title: "settings",
|
|
||||||
// align: 'center',
|
|
||||||
// width: 60,
|
|
||||||
// scopedSlots: { customRender: 'settings' },
|
|
||||||
// }, {
|
|
||||||
// title: "streamSettings",
|
|
||||||
// align: 'center',
|
|
||||||
// width: 60,
|
|
||||||
// scopedSlots: { customRender: 'streamSettings' },
|
|
||||||
}, {
|
}, {
|
||||||
title: "启用",
|
title: "详细信息",
|
||||||
|
align: 'center',
|
||||||
|
width: 40,
|
||||||
|
scopedSlots: { customRender: 'settings' },
|
||||||
|
}, {
|
||||||
|
title: "传输配置",
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 60,
|
width: 60,
|
||||||
scopedSlots: { customRender: 'enable' },
|
scopedSlots: { customRender: 'stream' },
|
||||||
}, {
|
}, {
|
||||||
title: "action",
|
title: "到期时间",
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 60,
|
width: 80,
|
||||||
scopedSlots: { customRender: 'action' },
|
scopedSlots: { customRender: 'expiryTime' },
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const app = new Vue({
|
const app = new Vue({
|
||||||
@@ -139,6 +178,7 @@
|
|||||||
data: {
|
data: {
|
||||||
siderDrawer,
|
siderDrawer,
|
||||||
spinning: false,
|
spinning: false,
|
||||||
|
inbounds: [],
|
||||||
dbInbounds: [],
|
dbInbounds: [],
|
||||||
searchKey: '',
|
searchKey: '',
|
||||||
},
|
},
|
||||||
@@ -156,9 +196,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) {
|
||||||
@@ -173,6 +216,22 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
clickAction(action, dbInbound) {
|
||||||
|
switch (action.key) {
|
||||||
|
case "qrcode":
|
||||||
|
this.showQrcode(dbInbound);
|
||||||
|
break;
|
||||||
|
case "edit":
|
||||||
|
this.openEditInbound(dbInbound);
|
||||||
|
break;
|
||||||
|
case "resetTraffic":
|
||||||
|
this.resetTraffic(dbInbound);
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
this.delInbound(dbInbound);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
openAddInbound() {
|
openAddInbound() {
|
||||||
inModal.show({
|
inModal.show({
|
||||||
title: '添加入站',
|
title: '添加入站',
|
||||||
@@ -205,6 +264,7 @@
|
|||||||
total: dbInbound.total,
|
total: dbInbound.total,
|
||||||
remark: dbInbound.remark,
|
remark: dbInbound.remark,
|
||||||
enable: dbInbound.enable,
|
enable: dbInbound.enable,
|
||||||
|
expiryTime: dbInbound.expiryTime,
|
||||||
|
|
||||||
listen: inbound.listen,
|
listen: inbound.listen,
|
||||||
port: inbound.port,
|
port: inbound.port,
|
||||||
@@ -222,6 +282,7 @@
|
|||||||
total: dbInbound.total,
|
total: dbInbound.total,
|
||||||
remark: dbInbound.remark,
|
remark: dbInbound.remark,
|
||||||
enable: dbInbound.enable,
|
enable: dbInbound.enable,
|
||||||
|
expiryTime: dbInbound.expiryTime,
|
||||||
|
|
||||||
listen: inbound.listen,
|
listen: inbound.listen,
|
||||||
port: inbound.port,
|
port: inbound.port,
|
||||||
@@ -256,13 +317,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 +361,6 @@
|
|||||||
{{template "promptModal"}}
|
{{template "promptModal"}}
|
||||||
{{template "qrcodeModal"}}
|
{{template "qrcodeModal"}}
|
||||||
{{template "textModal"}}
|
{{template "textModal"}}
|
||||||
|
{{template "inboundInfoModal"}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -188,8 +188,6 @@
|
|||||||
Object.freeze(State);
|
Object.freeze(State);
|
||||||
|
|
||||||
class CurTotal {
|
class CurTotal {
|
||||||
current = 0
|
|
||||||
total = 0
|
|
||||||
|
|
||||||
constructor(current, total) {
|
constructor(current, total) {
|
||||||
this.current = current;
|
this.current = current;
|
||||||
@@ -216,19 +214,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Status {
|
class Status {
|
||||||
cpu = new CurTotal(0, 0);
|
|
||||||
disk = new CurTotal(0, 0);
|
|
||||||
loads = [0, 0, 0];
|
|
||||||
mem = new CurTotal(0, 0);
|
|
||||||
netIO = {up: 0, down: 0};
|
|
||||||
netTraffic = {sent: 0, recv: 0};
|
|
||||||
swap = new CurTotal(0, 0);
|
|
||||||
tcpCount = 0;
|
|
||||||
udpCount = 0;
|
|
||||||
uptime = 0;
|
|
||||||
xray = {state: State.Stop, errorMsg: "", version: "", color: ""};
|
|
||||||
|
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
|
this.cpu = new CurTotal(0, 0);
|
||||||
|
this.disk = new CurTotal(0, 0);
|
||||||
|
this.loads = [0, 0, 0];
|
||||||
|
this.mem = new CurTotal(0, 0);
|
||||||
|
this.netIO = {up: 0, down: 0};
|
||||||
|
this.netTraffic = {sent: 0, recv: 0};
|
||||||
|
this.swap = new CurTotal(0, 0);
|
||||||
|
this.tcpCount = 0;
|
||||||
|
this.udpCount = 0;
|
||||||
|
this.uptime = 0;
|
||||||
|
this.xray = {state: State.Stop, errorMsg: "", version: "", color: ""};
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
25
web/job/check_inbound_job.go
Normal file
25
web/job/check_inbound_job.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package job
|
||||||
|
|
||||||
|
import (
|
||||||
|
"x-ui/logger"
|
||||||
|
"x-ui/web/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CheckInboundJob struct {
|
||||||
|
xrayService service.XrayService
|
||||||
|
inboundService service.InboundService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCheckInboundJob() *CheckInboundJob {
|
||||||
|
return new(CheckInboundJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *CheckInboundJob) Run() {
|
||||||
|
count, err := j.inboundService.DisableInvalidInbounds()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("disable invalid inbounds err:", err)
|
||||||
|
} else if count > 0 {
|
||||||
|
logger.Debugf("disabled %v inbounds", count)
|
||||||
|
j.xrayService.SetToNeedRestart()
|
||||||
|
}
|
||||||
|
}
|
||||||
25
web/job/check_xray_running_job.go
Normal file
25
web/job/check_xray_running_job.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package job
|
||||||
|
|
||||||
|
import "x-ui/web/service"
|
||||||
|
|
||||||
|
type CheckXrayRunningJob struct {
|
||||||
|
xrayService service.XrayService
|
||||||
|
|
||||||
|
checkTime int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCheckXrayRunningJob() *CheckXrayRunningJob {
|
||||||
|
return new(CheckXrayRunningJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *CheckXrayRunningJob) Run() {
|
||||||
|
if j.xrayService.IsXrayRunning() {
|
||||||
|
j.checkTime = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
j.checkTime++
|
||||||
|
if j.checkTime < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
j.xrayService.SetToNeedRestart()
|
||||||
|
}
|
||||||
30
web/job/xray_traffic_job.go
Normal file
30
web/job/xray_traffic_job.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package job
|
||||||
|
|
||||||
|
import (
|
||||||
|
"x-ui/logger"
|
||||||
|
"x-ui/web/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type XrayTrafficJob struct {
|
||||||
|
xrayService service.XrayService
|
||||||
|
inboundService service.InboundService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewXrayTrafficJob() *XrayTrafficJob {
|
||||||
|
return new(XrayTrafficJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *XrayTrafficJob) Run() {
|
||||||
|
if !j.xrayService.IsXrayRunning() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
traffics, err := j.xrayService.GetXrayTraffic()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("get xray traffic failed:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = j.inboundService.AddTraffic(traffics)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("add traffic failed:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
web/network/auto_https_listener.go
Normal file
21
web/network/auto_https_listener.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
type AutoHttpsListener struct {
|
||||||
|
net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAutoHttpsListener(listener net.Listener) net.Listener {
|
||||||
|
return &AutoHttpsListener{
|
||||||
|
Listener: listener,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AutoHttpsListener) Accept() (net.Conn, error) {
|
||||||
|
conn, err := l.Listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewAutoHttpsConn(conn), nil
|
||||||
|
}
|
||||||
67
web/network/autp_https_conn.go
Normal file
67
web/network/autp_https_conn.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AutoHttpsConn struct {
|
||||||
|
net.Conn
|
||||||
|
|
||||||
|
firstBuf []byte
|
||||||
|
bufStart int
|
||||||
|
|
||||||
|
readRequestOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAutoHttpsConn(conn net.Conn) net.Conn {
|
||||||
|
return &AutoHttpsConn{
|
||||||
|
Conn: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AutoHttpsConn) readRequest() bool {
|
||||||
|
c.firstBuf = make([]byte, 2048)
|
||||||
|
n, err := c.Conn.Read(c.firstBuf)
|
||||||
|
c.firstBuf = c.firstBuf[:n]
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
reader := bytes.NewReader(c.firstBuf)
|
||||||
|
bufReader := bufio.NewReader(reader)
|
||||||
|
request, err := http.ReadRequest(bufReader)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
resp := http.Response{
|
||||||
|
Header: http.Header{},
|
||||||
|
}
|
||||||
|
resp.StatusCode = http.StatusTemporaryRedirect
|
||||||
|
location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI)
|
||||||
|
resp.Header.Set("Location", location)
|
||||||
|
resp.Write(c.Conn)
|
||||||
|
c.Close()
|
||||||
|
c.firstBuf = nil
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AutoHttpsConn) Read(buf []byte) (int, error) {
|
||||||
|
c.readRequestOnce.Do(func() {
|
||||||
|
c.readRequest()
|
||||||
|
})
|
||||||
|
|
||||||
|
if c.firstBuf != nil {
|
||||||
|
n := copy(buf, c.firstBuf[c.bufStart:])
|
||||||
|
c.bufStart += n
|
||||||
|
if c.bufStart >= len(c.firstBuf) {
|
||||||
|
c.firstBuf = nil
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Conn.Read(buf)
|
||||||
|
}
|
||||||
@@ -3,8 +3,10 @@ package service
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
"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 +33,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 +107,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 +165,13 @@ 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()
|
||||||
|
now := time.Now()
|
||||||
result := db.Model(model.Inbound{}).
|
result := db.Model(model.Inbound{}).
|
||||||
Where("up + down >= total and total > 0 and enable = ?", true).
|
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
|
"x-ui/util/sys"
|
||||||
"x-ui/xray"
|
"x-ui/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -142,18 +143,14 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
|||||||
logger.Warning("can not find io counters")
|
logger.Warning("can not find io counters")
|
||||||
}
|
}
|
||||||
|
|
||||||
tcpConnStats, err := net.Connections("tcp")
|
status.TcpCount, err = sys.GetTCPCount()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("get connections failed:", err)
|
logger.Warning("get tcp connections failed:", err)
|
||||||
} else {
|
|
||||||
status.TcpCount = len(tcpConnStats)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
udpConnStats, err := net.Connections("udp")
|
status.UdpCount, err = sys.GetUDPCount()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("get connections failed:", err)
|
logger.Warning("get udp connections failed:", err)
|
||||||
} else {
|
|
||||||
status.UdpCount = len(udpConnStats)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.xrayService.IsXrayRunning() {
|
if s.xrayService.IsXrayRunning() {
|
||||||
@@ -265,7 +262,7 @@ func (s *ServerService) UpdateXray(version string) error {
|
|||||||
|
|
||||||
s.xrayService.StopXray()
|
s.xrayService.StopXray()
|
||||||
defer func() {
|
defer func() {
|
||||||
err := s.xrayService.RestartXray()
|
err := s.xrayService.RestartXray(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("start xray failed:", err)
|
logger.Error("start xray failed:", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -84,17 +84,19 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, error) {
|
|||||||
return p.GetTraffic(true)
|
return p.GetTraffic(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *XrayService) RestartXray() error {
|
func (s *XrayService) RestartXray(isForce bool) 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 {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if p != nil {
|
if p != nil && p.IsRunning() {
|
||||||
if p.GetConfig().Equals(xrayConfig) {
|
if !isForce && p.GetConfig().Equals(xrayConfig) {
|
||||||
|
logger.Debug("not need to restart xray")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
p.Stop()
|
p.Stop()
|
||||||
@@ -108,16 +110,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)
|
||||||
}
|
}
|
||||||
|
|||||||
122
web/web.go
122
web/web.go
@@ -18,11 +18,14 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"x-ui/config"
|
"x-ui/config"
|
||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
"x-ui/util/common"
|
"x-ui/util/common"
|
||||||
"x-ui/web/controller"
|
"x-ui/web/controller"
|
||||||
|
"x-ui/web/job"
|
||||||
|
"x-ui/web/network"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,16 +38,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 +164,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
|
||||||
@@ -238,52 +279,21 @@ func (s *Server) initI18n(engine *gin.Engine) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) startTask() {
|
func (s *Server) startTask() {
|
||||||
err := s.xrayService.RestartXray()
|
err := s.xrayService.RestartXray(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("start xray failed:", err)
|
logger.Warning("start xray failed:", err)
|
||||||
}
|
}
|
||||||
var checkTime = 0
|
|
||||||
// 每 30 秒检查一次 xray 是否在运行
|
// 每 30 秒检查一次 xray 是否在运行
|
||||||
s.cron.AddFunc("@every 30s", func() {
|
s.cron.AddJob("@every 30s", job.NewCheckXrayRunningJob())
|
||||||
if s.xrayService.IsXrayRunning() {
|
|
||||||
checkTime = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
checkTime++
|
|
||||||
if checkTime < 2 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.xrayService.SetIsNeedRestart(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(time.Second * 5)
|
time.Sleep(time.Second * 5)
|
||||||
// 每 10 秒统计一次流量,首次启动延迟 5 秒,与重启 xray 的时间错开
|
// 每 10 秒统计一次流量,首次启动延迟 5 秒,与重启 xray 的时间错开
|
||||||
s.cron.AddFunc("@every 10s", func() {
|
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
|
||||||
if !s.xrayService.IsXrayRunning() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
traffics, err := s.xrayService.GetXrayTraffic()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("get xray traffic failed:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = s.inboundService.AddTraffic(traffics)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("add traffic failed:", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// 每分钟检查一次 inbound 流量超出情况
|
// 每 30 秒检查一次 inbound 流量超出和到期的情况
|
||||||
s.cron.AddFunc("@every 1m", func() {
|
s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
|
||||||
needRestart, err := s.inboundService.DisableInvalidInbounds()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("disable invalid inbounds err:", err)
|
|
||||||
} else if needRestart {
|
|
||||||
s.xrayService.SetIsNeedRestart(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start() (err error) {
|
func (s *Server) Start() (err error) {
|
||||||
@@ -322,22 +332,21 @@ func (s *Server) Start() (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
|
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
|
||||||
var listener net.Listener
|
listener, err := net.Listen("tcp", listenAddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if certFile != "" || keyFile != "" {
|
if certFile != "" || keyFile != "" {
|
||||||
var cert tls.Certificate
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
listener.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c := &tls.Config{
|
c := &tls.Config{
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
}
|
}
|
||||||
listener, err = tls.Listen("tcp", listenAddr, c)
|
listener = network.NewAutoHttpsListener(listener)
|
||||||
} else {
|
listener = tls.NewListener(listener, c)
|
||||||
listener, err = net.Listen("tcp", listenAddr)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if certFile != "" || keyFile != "" {
|
if certFile != "" || keyFile != "" {
|
||||||
logger.Info("web server run https on", listener.Addr())
|
logger.Info("web server run https on", listener.Addr())
|
||||||
@@ -348,7 +357,13 @@ func (s *Server) Start() (err error) {
|
|||||||
|
|
||||||
s.startTask()
|
s.startTask()
|
||||||
|
|
||||||
go engine.RunListener(listener)
|
s.httpServer = &http.Server{
|
||||||
|
Handler: engine,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
s.httpServer.Serve(listener)
|
||||||
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -359,10 +374,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 {
|
||||||
|
|||||||
11
x-ui.sh
11
x-ui.sh
@@ -101,7 +101,7 @@ update() {
|
|||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
bash <(curl -Ls https://blog.sprov.xyz/x-ui.sh)
|
bash <(curl -Ls https://raw.githubusercontent.com/sprov065/x-ui/master/install.sh)
|
||||||
if [[ $? == 0 ]]; then
|
if [[ $? == 0 ]]; then
|
||||||
echo -e "${green}更新完成,已自动重启面板${plain}"
|
echo -e "${green}更新完成,已自动重启面板${plain}"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -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
|
||||||
;;
|
;;
|
||||||
|
"v2-ui") 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
|
||||||
|
|||||||
Reference in New Issue
Block a user