mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-27 02:24:40 +00:00
Compare commits
13 commits
bee4c75a5a
...
2218cfba56
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2218cfba56 | ||
|
|
db7e7dcd29 | ||
|
|
01b8a27996 | ||
|
|
3764ece26c | ||
|
|
d7efc2aef9 | ||
|
|
2eb8abf61e | ||
|
|
5e953bae45 | ||
|
|
747af376f2 | ||
|
|
a3ccccfe52 | ||
|
|
3299d15f28 | ||
|
|
ae82373457 | ||
|
|
d65233cc2c | ||
|
|
11dc06863e |
24 changed files with 853 additions and 113 deletions
|
|
@ -1 +1 @@
|
||||||
2.8.0
|
2.8.1
|
||||||
|
|
@ -35,6 +35,7 @@ func initModels() error {
|
||||||
&model.InboundClientIps{},
|
&model.InboundClientIps{},
|
||||||
&xray.ClientTraffic{},
|
&xray.ClientTraffic{},
|
||||||
&model.HistoryOfSeeders{},
|
&model.HistoryOfSeeders{},
|
||||||
|
&model.Server{},
|
||||||
}
|
}
|
||||||
for _, model := range models {
|
for _, model := range models {
|
||||||
if err := db.AutoMigrate(model); err != nil {
|
if err := db.AutoMigrate(model); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -115,3 +115,12 @@ type VLESSSettings struct {
|
||||||
Encryption string `json:"encryption"`
|
Encryption string `json:"encryption"`
|
||||||
Fallbacks []any `json:"fallbacks"`
|
Fallbacks []any `json:"fallbacks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
Name string `json:"name" gorm:"unique;not null"`
|
||||||
|
Address string `json:"address" gorm:"not null"`
|
||||||
|
Port int `json:"port" gorm:"not null"`
|
||||||
|
APIKey string `json:"apiKey" gorm:"not null"`
|
||||||
|
Enable bool `json:"enable" gorm:"default:true"`
|
||||||
|
}
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -64,6 +64,7 @@ require (
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,13 @@ config_after_install() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
/usr/local/x-ui/x-ui migrate
|
/usr/local/x-ui/x-ui migrate
|
||||||
|
|
||||||
|
local existing_apiKey=$(/usr/local/x-ui/x-ui setting -show true | grep -oP 'ApiKey: \K.*')
|
||||||
|
if [[ -z "$existing_apiKey" ]]; then
|
||||||
|
local config_apiKey=$(gen_random_string 32)
|
||||||
|
/usr/local/x-ui/x-ui setting -apiKey "${config_apiKey}"
|
||||||
|
echo -e "${green}Generated random API Key: ${config_apiKey}${plain}"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
install_x-ui() {
|
install_x-ui() {
|
||||||
|
|
|
||||||
15
main.go
15
main.go
|
|
@ -232,7 +232,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) {
|
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool, apiKey string) {
|
||||||
err := database.InitDB(config.GetDBPath())
|
err := database.InitDB(config.GetDBPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Database initialization failed:", err)
|
fmt.Println("Database initialization failed:", err)
|
||||||
|
|
@ -242,6 +242,15 @@ func updateSetting(port int, username string, password string, webBasePath strin
|
||||||
settingService := service.SettingService{}
|
settingService := service.SettingService{}
|
||||||
userService := service.UserService{}
|
userService := service.UserService{}
|
||||||
|
|
||||||
|
if apiKey != "" {
|
||||||
|
err := settingService.SetAPIKey(apiKey)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to set API Key:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("API Key set successfully: %v\n", apiKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if port > 0 {
|
if port > 0 {
|
||||||
err := settingService.SetPort(port)
|
err := settingService.SetPort(port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -388,9 +397,11 @@ func main() {
|
||||||
var show bool
|
var show bool
|
||||||
var getCert bool
|
var getCert bool
|
||||||
var resetTwoFactor bool
|
var resetTwoFactor bool
|
||||||
|
var apiKey string
|
||||||
settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
|
settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
|
||||||
settingCmd.BoolVar(&show, "show", false, "Display current settings")
|
settingCmd.BoolVar(&show, "show", false, "Display current settings")
|
||||||
settingCmd.IntVar(&port, "port", 0, "Set panel port number")
|
settingCmd.IntVar(&port, "port", 0, "Set panel port number")
|
||||||
|
settingCmd.StringVar(&apiKey, "apiKey", "", "Set API Key")
|
||||||
settingCmd.StringVar(&username, "username", "", "Set login username")
|
settingCmd.StringVar(&username, "username", "", "Set login username")
|
||||||
settingCmd.StringVar(&password, "password", "", "Set login password")
|
settingCmd.StringVar(&password, "password", "", "Set login password")
|
||||||
settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel")
|
settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel")
|
||||||
|
|
@ -440,7 +451,7 @@ func main() {
|
||||||
if reset {
|
if reset {
|
||||||
resetSetting()
|
resetSetting()
|
||||||
} else {
|
} else {
|
||||||
updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor)
|
updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor, apiKey)
|
||||||
}
|
}
|
||||||
if show {
|
if show {
|
||||||
showSetting(show)
|
showSetting(show)
|
||||||
|
|
|
||||||
|
|
@ -159,26 +159,43 @@ func (s *SubService) getFallbackMaster(dest string, streamSettings string) (stri
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
||||||
switch inbound.Protocol {
|
serverService := service.MultiServerService{}
|
||||||
case "vmess":
|
servers, err := serverService.GetServers()
|
||||||
return s.genVmessLink(inbound, email)
|
if err != nil {
|
||||||
case "vless":
|
logger.Warning("Failed to get servers for subscription:", err)
|
||||||
return s.genVlessLink(inbound, email)
|
|
||||||
case "trojan":
|
|
||||||
return s.genTrojanLink(inbound, email)
|
|
||||||
case "shadowsocks":
|
|
||||||
return s.genShadowsocksLink(inbound, email)
|
|
||||||
}
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
var links []string
|
||||||
|
for _, server := range servers {
|
||||||
|
if !server.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var link string
|
||||||
|
switch inbound.Protocol {
|
||||||
|
case "vmess":
|
||||||
|
link = s.genVmessLink(inbound, email, server)
|
||||||
|
case "vless":
|
||||||
|
link = s.genVlessLink(inbound, email, server)
|
||||||
|
case "trojan":
|
||||||
|
link = s.genTrojanLink(inbound, email, server)
|
||||||
|
case "shadowsocks":
|
||||||
|
link = s.genShadowsocksLink(inbound, email, server)
|
||||||
|
}
|
||||||
|
if link != "" {
|
||||||
|
links = append(links, link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(links, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) genVmessLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||||
if inbound.Protocol != model.VMESS {
|
if inbound.Protocol != model.VMESS {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
obj := map[string]any{
|
obj := map[string]any{
|
||||||
"v": "2",
|
"v": "2",
|
||||||
"add": s.address,
|
"add": server.Address,
|
||||||
"port": inbound.Port,
|
"port": inbound.Port,
|
||||||
"type": "none",
|
"type": "none",
|
||||||
}
|
}
|
||||||
|
|
@ -291,7 +308,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||||
newObj[key] = value
|
newObj[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string))
|
newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||||
newObj["add"] = ep["dest"].(string)
|
newObj["add"] = ep["dest"].(string)
|
||||||
newObj["port"] = int(ep["port"].(float64))
|
newObj["port"] = int(ep["port"].(float64))
|
||||||
|
|
||||||
|
|
@ -307,14 +324,14 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||||
return links
|
return links
|
||||||
}
|
}
|
||||||
|
|
||||||
obj["ps"] = s.genRemark(inbound, email, "")
|
obj["ps"] = s.genRemark(inbound, email, "", server.Name)
|
||||||
|
|
||||||
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||||
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) genVlessLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||||
address := s.address
|
address := server.Address
|
||||||
if inbound.Protocol != model.VLESS {
|
if inbound.Protocol != model.VLESS {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -493,7 +510,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||||
|
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
links += "\n"
|
links += "\n"
|
||||||
|
|
@ -514,12 +531,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, "")
|
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||||
return url.String()
|
return url.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||||
address := s.address
|
address := server.Address
|
||||||
if inbound.Protocol != model.Trojan {
|
if inbound.Protocol != model.Trojan {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -688,7 +705,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||||
|
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
links += "\n"
|
links += "\n"
|
||||||
|
|
@ -710,12 +727,12 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, "")
|
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||||
return url.String()
|
return url.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||||
address := s.address
|
address := server.Address
|
||||||
if inbound.Protocol != model.Shadowsocks {
|
if inbound.Protocol != model.Shadowsocks {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -855,7 +872,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||||
|
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
links += "\n"
|
links += "\n"
|
||||||
|
|
@ -876,17 +893,18 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, "")
|
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||||
return url.String()
|
return url.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
|
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string, serverName string) string {
|
||||||
separationChar := string(s.remarkModel[0])
|
separationChar := string(s.remarkModel[0])
|
||||||
orderChars := s.remarkModel[1:]
|
orderChars := s.remarkModel[1:]
|
||||||
orders := map[byte]string{
|
orders := map[byte]string{
|
||||||
'i': "",
|
'i': "",
|
||||||
'e': "",
|
'e': "",
|
||||||
'o': "",
|
'o': "",
|
||||||
|
's': "",
|
||||||
}
|
}
|
||||||
if len(email) > 0 {
|
if len(email) > 0 {
|
||||||
orders['e'] = email
|
orders['e'] = email
|
||||||
|
|
@ -897,6 +915,9 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin
|
||||||
if len(extra) > 0 {
|
if len(extra) > 0 {
|
||||||
orders['o'] = extra
|
orders['o'] = extra
|
||||||
}
|
}
|
||||||
|
if len(serverName) > 0 {
|
||||||
|
orders['s'] = serverName
|
||||||
|
}
|
||||||
|
|
||||||
var remark []string
|
var remark []string
|
||||||
for i := 0; i < len(orderChars); i++ {
|
for i := 0; i < len(orderChars); i++ {
|
||||||
|
|
|
||||||
2
web/assets/css/custom.min.css
vendored
2
web/assets/css/custom.min.css
vendored
File diff suppressed because one or more lines are too long
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"x-ui/database/model"
|
"x-ui/database/model"
|
||||||
|
"x-ui/web/middleware"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
"x-ui/web/session"
|
"x-ui/web/session"
|
||||||
|
|
||||||
|
|
|
||||||
89
web/controller/multi_server_controller.go
Normal file
89
web/controller/multi_server_controller.go
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"x-ui/database/model"
|
||||||
|
"x-ui/web/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MultiServerController struct {
|
||||||
|
multiServerService service.MultiServerService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMultiServerController(g *gin.RouterGroup) *MultiServerController {
|
||||||
|
c := &MultiServerController{}
|
||||||
|
c.initRouter(g)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiServerController) initRouter(g *gin.RouterGroup) {
|
||||||
|
g = g.Group("/server")
|
||||||
|
|
||||||
|
g.GET("/list", c.getServers)
|
||||||
|
g.POST("/add", c.addServer)
|
||||||
|
g.POST("/del/:id", c.delServer)
|
||||||
|
g.POST("/update/:id", c.updateServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiServerController) getServers(ctx *gin.Context) {
|
||||||
|
servers, err := c.multiServerService.GetServers()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Failed to get servers", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(ctx, servers, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiServerController) addServer(ctx *gin.Context) {
|
||||||
|
server := &model.Server{}
|
||||||
|
err := ctx.ShouldBind(server)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Invalid data", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = c.multiServerService.AddServer(server)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Failed to add server", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(ctx, "Server added successfully", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiServerController) delServer(ctx *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(ctx.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Invalid ID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = c.multiServerService.DeleteServer(id)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Failed to delete server", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(ctx, "Server deleted successfully", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiServerController) updateServer(ctx *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(ctx.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Invalid ID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
server := &model.Server{
|
||||||
|
Id: id,
|
||||||
|
}
|
||||||
|
err = ctx.ShouldBind(server)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Invalid data", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = c.multiServerService.UpdateServer(server)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Failed to update server", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(ctx, "Server updated successfully", nil)
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||||
|
|
||||||
g.GET("/", a.index)
|
g.GET("/", a.index)
|
||||||
g.GET("/inbounds", a.inbounds)
|
g.GET("/inbounds", a.inbounds)
|
||||||
|
g.GET("/servers", a.servers)
|
||||||
g.GET("/settings", a.settings)
|
g.GET("/settings", a.settings)
|
||||||
g.GET("/xray", a.xraySettings)
|
g.GET("/xray", a.xraySettings)
|
||||||
|
|
||||||
|
|
@ -49,3 +50,7 @@ func (a *XUIController) settings(c *gin.Context) {
|
||||||
func (a *XUIController) xraySettings(c *gin.Context) {
|
func (a *XUIController) xraySettings(c *gin.Context) {
|
||||||
html(c, "xray.html", "pages.xray.title", nil)
|
html(c, "xray.html", "pages.xray.title", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *XUIController) servers(c *gin.Context) {
|
||||||
|
html(c, "servers.html", "Servers", nil)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,11 @@
|
||||||
icon: 'user',
|
icon: 'user',
|
||||||
title: '{{ i18n "menu.inbounds"}}'
|
title: '{{ i18n "menu.inbounds"}}'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: '{{ .base_path }}panel/servers',
|
||||||
|
icon: 'cloud-server',
|
||||||
|
title: 'Servers'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: '{{ .base_path }}panel/settings',
|
key: '{{ .base_path }}panel/settings',
|
||||||
icon: 'setting',
|
icon: 'setting',
|
||||||
|
|
|
||||||
|
|
@ -568,8 +568,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ i18n "pages.inbounds.periodicTrafficResetTitle" }}</td>
|
<td>{{ i18n "pages.inbounds.periodicTrafficResetTitle" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a-tag color="blue">[[ i18n("pages.inbounds.periodicTrafficReset." +
|
<a-tag color="blue">[[ dbInbound.trafficReset ]]</a-tag>
|
||||||
dbInbound.trafficReset) ]]</a-tag>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
</template>
|
</template>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
|
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
|
||||||
<a-button size="small" type="default" class="ml-8" @click="openCpuHistory()">
|
<a-button size="small" shape="circle" class="ml-8" @click="openCpuHistory()">
|
||||||
<a-icon type="history" />
|
<a-icon type="history" />
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
|
@ -343,7 +343,7 @@
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item class="mr-05">
|
<a-form-item class="mr-05">
|
||||||
<a-input-group compact>
|
<a-input-group compact>
|
||||||
<a-select size="small" v-model="logModal.rows" class="w-70" @change="openLogs()"
|
<a-select size="small" v-model="logModal.rows" :style="{ width: '70px' }" @change="openLogs()"
|
||||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option value="10">10</a-select-option>
|
<a-select-option value="10">10</a-select-option>
|
||||||
<a-select-option value="20">20</a-select-option>
|
<a-select-option value="20">20</a-select-option>
|
||||||
|
|
@ -351,7 +351,7 @@
|
||||||
<a-select-option value="100">100</a-select-option>
|
<a-select-option value="100">100</a-select-option>
|
||||||
<a-select-option value="500">500</a-select-option>
|
<a-select-option value="500">500</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
<a-select size="small" v-model="logModal.level" class="w-95" @change="openLogs()"
|
<a-select size="small" v-model="logModal.level" :style="{ width: '95px' }" @change="openLogs()"
|
||||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option value="debug">Debug</a-select-option>
|
<a-select-option value="debug">Debug</a-select-option>
|
||||||
<a-select-option value="info">Info</a-select-option>
|
<a-select-option value="info">Info</a-select-option>
|
||||||
|
|
@ -365,8 +365,7 @@
|
||||||
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
|
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item style="float: right;">
|
<a-form-item style="float: right;">
|
||||||
<a-button type="primary" icon="download"
|
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
|
||||||
@click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
<div class="ant-input log-container" v-html="logModal.formattedLogs"></div>
|
<div class="ant-input log-container" v-html="logModal.formattedLogs"></div>
|
||||||
|
|
@ -382,7 +381,7 @@
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item class="mr-05">
|
<a-form-item class="mr-05">
|
||||||
<a-input-group compact>
|
<a-input-group compact>
|
||||||
<a-select size="small" v-model="xraylogModal.rows" class="w-70" @change="openXrayLogs()"
|
<a-select size="small" v-model="xraylogModal.rows" :style="{ width: '70px' }" @change="openXrayLogs()"
|
||||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option value="10">10</a-select-option>
|
<a-select-option value="10">10</a-select-option>
|
||||||
<a-select-option value="20">20</a-select-option>
|
<a-select-option value="20">20</a-select-option>
|
||||||
|
|
@ -401,8 +400,7 @@
|
||||||
<a-checkbox v-model="xraylogModal.showProxy" @change="openXrayLogs()">Proxy</a-checkbox>
|
<a-checkbox v-model="xraylogModal.showProxy" @change="openXrayLogs()">Proxy</a-checkbox>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item style="float: right;">
|
<a-form-item style="float: right;">
|
||||||
<a-button type="primary" icon="download"
|
<a-button type="primary" icon="download" @click="downloadXrayLogs"></a-button>
|
||||||
@click="FileManager.downloadTextFile(xraylogModal.logs?.join('\n'), 'x-ui.log')"></a-button>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
<div class="ant-input log-container" v-html="xraylogModal.formattedLogs"></div>
|
<div class="ant-input log-container" v-html="xraylogModal.formattedLogs"></div>
|
||||||
|
|
@ -431,7 +429,7 @@
|
||||||
@cancel="() => cpuHistoryModal.visible = false" :class="themeSwitcher.currentTheme" width="900px" footer="">
|
@cancel="() => cpuHistoryModal.visible = false" :class="themeSwitcher.currentTheme" width="900px" footer="">
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
CPU History
|
CPU History
|
||||||
<a-select size="small" v-model="cpuHistoryModal.bucket" class="ml-10" style="width: 140px"
|
<a-select size="small" v-model="cpuHistoryModal.bucket" class="ml-10" style="width: 80px"
|
||||||
@change="fetchCpuHistoryBucket">
|
@change="fetchCpuHistoryBucket">
|
||||||
<a-select-option :value="2">2s</a-select-option>
|
<a-select-option :value="2">2s</a-select-option>
|
||||||
<a-select-option :value="30">30s</a-select-option>
|
<a-select-option :value="30">30s</a-select-option>
|
||||||
|
|
@ -441,7 +439,7 @@
|
||||||
<a-select-option :value="300">5m</a-select-option>
|
<a-select-option :value="300">5m</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</template>
|
</template>
|
||||||
<div style="padding: 8px 0;">
|
<div style="padding:16px">
|
||||||
<sparkline :data="cpuHistoryLong" :labels="cpuHistoryLabels" :vb-width="840" :height="220"
|
<sparkline :data="cpuHistoryLong" :labels="cpuHistoryLabels" :vb-width="840" :height="220"
|
||||||
:stroke="status.cpu.color" :stroke-width="2.2" :show-grid="true" :show-axes="true" :tick-count-x="5"
|
:stroke="status.cpu.color" :stroke-width="2.2" :show-grid="true" :show-axes="true" :tick-count-x="5"
|
||||||
:max-points="cpuHistoryLong.length" :fill-opacity="0.18" :marker-radius="3.2" :show-tooltip="true" />
|
:max-points="cpuHistoryLong.length" :fill-opacity="0.18" :marker-radius="3.2" :show-tooltip="true" />
|
||||||
|
|
@ -466,7 +464,7 @@
|
||||||
strokeWidth: { type: Number, default: 2 },
|
strokeWidth: { type: Number, default: 2 },
|
||||||
maxPoints: { type: Number, default: 120 },
|
maxPoints: { type: Number, default: 120 },
|
||||||
showGrid: { type: Boolean, default: true },
|
showGrid: { type: Boolean, default: true },
|
||||||
gridColor: { type: String, default: 'rgba(255,255,255,0.08)' },
|
gridColor: { type: String, default: 'rgba(0,0,0,0.1)' },
|
||||||
fillOpacity: { type: Number, default: 0.15 },
|
fillOpacity: { type: Number, default: 0.15 },
|
||||||
showMarker: { type: Boolean, default: true },
|
showMarker: { type: Boolean, default: true },
|
||||||
markerRadius: { type: Number, default: 2.8 },
|
markerRadius: { type: Number, default: 2.8 },
|
||||||
|
|
@ -540,7 +538,7 @@
|
||||||
const h = this.drawHeight
|
const h = this.drawHeight
|
||||||
const w = this.drawWidth
|
const w = this.drawWidth
|
||||||
// draw at 25%, 50%, 75%
|
// draw at 25%, 50%, 75%
|
||||||
return [0.25, 0.5, 0.75]
|
return [0, 0.25, 0.5, 0.75, 1]
|
||||||
.map(r => Math.round(this.paddingTop + h * r))
|
.map(r => Math.round(this.paddingTop + h * r))
|
||||||
.map(y => ({ x1: this.paddingLeft, y1: y, x2: this.paddingLeft + w, y2: y }))
|
.map(y => ({ x1: this.paddingLeft, y1: y, x2: this.paddingLeft + w, y2: y }))
|
||||||
},
|
},
|
||||||
|
|
@ -606,7 +604,7 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<svg width="100%" :height="height" :viewBox="viewBoxAttr" preserveAspectRatio="none" style="display:block"
|
<svg width="100%" :height="height" :viewBox="viewBoxAttr" preserveAspectRatio="none" class="idx-cpu-history-svg"
|
||||||
@mousemove="onMouseMove" @mouseleave="onMouseLeave">
|
@mousemove="onMouseMove" @mouseleave="onMouseLeave">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="spkGrad" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="spkGrad" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
|
@ -615,16 +613,16 @@
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<g v-if="showGrid">
|
<g v-if="showGrid">
|
||||||
<line v-for="(g,i) in gridLines" :key="i" :x1="g.x1" :y1="g.y1" :x2="g.x2" :y2="g.y2" :stroke="gridColor" stroke-width="1"/>
|
<line v-for="(g,i) in gridLines" :key="i" :x1="g.x1" :y1="g.y1" :x2="g.x2" :y2="g.y2" :stroke="gridColor" stroke-width="1" class="cpu-grid-line" />
|
||||||
</g>
|
</g>
|
||||||
<g v-if="showAxes">
|
<g v-if="showAxes">
|
||||||
<!-- Y ticks/labels -->
|
<!-- Y ticks/labels -->
|
||||||
<g v-for="(t,i) in yTicks" :key="'y'+i">
|
<g v-for="(t,i) in yTicks" :key="'y'+i">
|
||||||
<text :x="Math.max(0, paddingLeft - 4)" :y="t.y + 4" text-anchor="end" font-size="10" fill="rgba(200,200,200,0.8)" v-text="t.label"></text>
|
<text class="cpu-grid-y-text" :x="Math.max(0, paddingLeft - 4)" :y="t.y + 4" text-anchor="end" font-size="10" fill="rgba(0,0,0,0.3)" v-text="t.label"></text>
|
||||||
</g>
|
</g>
|
||||||
<!-- X ticks/labels -->
|
<!-- X ticks/labels -->
|
||||||
<g v-for="(t,i) in xTicks" :key="'x'+i">
|
<g v-for="(t,i) in xTicks" :key="'x'+i">
|
||||||
<text :x="t.x" :y="paddingTop + drawHeight + 14" text-anchor="middle" font-size="10" fill="rgba(200,200,200,0.8)" v-text="t.label"></text>
|
<text class="cpu-grid-x-text" :x="t.x" :y="paddingTop + drawHeight + 22" text-anchor="middle" font-size="10" fill="rgba(0,0,0,0.3)" v-text="t.label"></text>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<path v-if="areaPath" :d="areaPath" fill="url(#spkGrad)" stroke="none" />
|
<path v-if="areaPath" :d="areaPath" fill="url(#spkGrad)" stroke="none" />
|
||||||
|
|
@ -632,9 +630,9 @@
|
||||||
<circle v-if="showMarker && lastPoint" :cx="lastPoint[0]" :cy="lastPoint[1]" :r="markerRadius" :fill="stroke" />
|
<circle v-if="showMarker && lastPoint" :cx="lastPoint[0]" :cy="lastPoint[1]" :r="markerRadius" :fill="stroke" />
|
||||||
<!-- Hover marker/tooltip -->
|
<!-- Hover marker/tooltip -->
|
||||||
<g v-if="showTooltip && hoverIdx >= 0">
|
<g v-if="showTooltip && hoverIdx >= 0">
|
||||||
<line :x1="pointsArr[hoverIdx][0]" :x2="pointsArr[hoverIdx][0]" :y1="paddingTop" :y2="paddingTop + drawHeight" stroke="rgba(255,255,255,0.25)" stroke-width="1" />
|
<line class="cpu-grid-h-line" :x1="pointsArr[hoverIdx][0]" :x2="pointsArr[hoverIdx][0]" :y1="paddingTop" :y2="paddingTop + drawHeight" stroke="rgba(0,0,0,0.2)" stroke-width="1" />
|
||||||
<circle :cx="pointsArr[hoverIdx][0]" :cy="pointsArr[hoverIdx][1]" r="3.5" :fill="stroke" />
|
<circle :cx="pointsArr[hoverIdx][0]" :cy="pointsArr[hoverIdx][1]" r="3.5" :fill="stroke" />
|
||||||
<text :x="pointsArr[hoverIdx][0]" :y="paddingTop + 12" text-anchor="middle" font-size="11" fill="#fff" style="paint-order: stroke; stroke: rgba(0,0,0,0.35); stroke-width: 3;" v-text="fmtHoverText()"></text>
|
<text class="cpu-grid-text" :x="pointsArr[hoverIdx][0]" :y="paddingTop + 12" text-anchor="middle" font-size="11" fill="rgba(0,0,0,0.8)" v-text="fmtHoverText()"></text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
`,
|
`,
|
||||||
|
|
@ -809,46 +807,61 @@
|
||||||
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
|
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
|
||||||
},
|
},
|
||||||
formatLogs(logs) {
|
formatLogs(logs) {
|
||||||
let formattedLogs = '';
|
let formattedLogs = `
|
||||||
|
<style>
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
logs.forEach((log, index) => {
|
table td, table th {
|
||||||
if (index > 0) formattedLogs += '<br>';
|
padding: 2px 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
const parts = log.split(' ');
|
<table>
|
||||||
|
<tr>
|
||||||
if (parts.length === 10) {
|
<th>Date</th>
|
||||||
const dateTime = `<b>${parts[0]} ${parts[1]}</b>`;
|
<th>From</th>
|
||||||
const from = `<b>${parts[3]}</b>`;
|
<th>To</th>
|
||||||
const to = `<b>${parts[5].replace(/^\/+/, "")}</b>`;
|
<th>Inbound</th>
|
||||||
|
<th>Outbound</th>
|
||||||
|
<th>Email</th>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
|
||||||
|
logs.reverse().forEach((log, index) => {
|
||||||
let outboundColor = '';
|
let outboundColor = '';
|
||||||
if (parts[9] === "b") {
|
if (log.Event === 1) {
|
||||||
outboundColor = ' style="color: #e04141;"'; //red for blocked
|
outboundColor = ' style="color: #e04141;"'; //red for blocked
|
||||||
}
|
}
|
||||||
else if (parts[9] === "p") {
|
else if (log.Event === 2) {
|
||||||
outboundColor = ' style="color: #3c89e8;"'; //blue for proxies
|
outboundColor = ' style="color: #3c89e8;"'; //blue for proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
formattedLogs += `<span${outboundColor}>
|
let text = ``;
|
||||||
${dateTime}
|
if (log.Email !== "") {
|
||||||
${parts[2]}
|
text = `<td>${log.Email}</td>`;
|
||||||
${from}
|
|
||||||
${parts[4]}
|
|
||||||
${to}
|
|
||||||
${parts.slice(6, 9).join(' ')}
|
|
||||||
</span>`;
|
|
||||||
} else {
|
|
||||||
formattedLogs += `<span>${log}</span>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formattedLogs += `
|
||||||
|
<tr ${outboundColor}>
|
||||||
|
<td><b>${new Date(log.DateTime).toLocaleString()}</b></td>
|
||||||
|
<td>${log.FromAddress}</td>
|
||||||
|
<td>${log.ToAddress}</td>
|
||||||
|
<td>${log.Inbound}</td>
|
||||||
|
<td>${log.Outbound}</td>
|
||||||
|
${text}
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
return formattedLogs;
|
return formattedLogs += "</table>";
|
||||||
},
|
},
|
||||||
hide() {
|
hide() {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const backupModal = {
|
const backupModal = {
|
||||||
visible: false,
|
visible: false,
|
||||||
show() {
|
show() {
|
||||||
|
|
@ -1023,6 +1036,25 @@ ${dateTime}
|
||||||
await PromiseUtil.sleep(500);
|
await PromiseUtil.sleep(500);
|
||||||
xraylogModal.loading = false;
|
xraylogModal.loading = false;
|
||||||
},
|
},
|
||||||
|
downloadXrayLogs() {
|
||||||
|
if (!Array.isArray(this.xraylogModal.logs) || this.xraylogModal.logs.length === 0) {
|
||||||
|
FileManager.downloadTextFile('', 'x-ui.log');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = this.xraylogModal.logs.map(l => {
|
||||||
|
try {
|
||||||
|
const dt = l.DateTime ? new Date(l.DateTime) : null;
|
||||||
|
const dateStr = dt && !isNaN(dt.getTime()) ? dt.toISOString() : '';
|
||||||
|
const eventMap = { 0: 'DIRECT', 1: 'BLOCKED', 2: 'PROXY' };
|
||||||
|
const eventText = eventMap[l.Event] || String(l.Event ?? '');
|
||||||
|
const emailPart = l.Email ? ` Email=${l.Email}` : '';
|
||||||
|
return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`.trim();
|
||||||
|
} catch (e) {
|
||||||
|
return JSON.stringify(l);
|
||||||
|
}
|
||||||
|
}).join('\n');
|
||||||
|
FileManager.downloadTextFile(lines, 'x-ui.log');
|
||||||
|
},
|
||||||
async openConfig() {
|
async openConfig() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
|
const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
|
||||||
|
|
@ -1071,7 +1103,6 @@ ${dateTime}
|
||||||
fileInput.click();
|
fileInput.click();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {},
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
if (window.location.protocol !== "https:") {
|
if (window.location.protocol !== "https:") {
|
||||||
this.showAlert = true;
|
this.showAlert = true;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
{{ template "page/body_start" .}}
|
{{ template "page/body_start" .}}
|
||||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
|
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-layout-content class="under min-h-100vh">
|
<a-layout-content class="under min-h-0">
|
||||||
<div class="waves-header">
|
<div class="waves-header">
|
||||||
<div class="waves-inner-header"></div>
|
<div class="waves-inner-header"></div>
|
||||||
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<a-row type="flex" justify="center" align="middle" class="h-100 overflow-hidden-auto">
|
<a-row type="flex" justify="center" align="middle" class="h-100 overflow-y-auto overflow-x-hidden">
|
||||||
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" class="my-3rem">
|
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" class="my-3rem">
|
||||||
<template v-if="!loadingStates.fetched">
|
<template v-if="!loadingStates.fetched">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|
@ -184,5 +184,80 @@
|
||||||
newWord.classList.add('is-visible');
|
newWord.classList.add('is-visible');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pm_input_selector = 'input.ant-input, textarea.ant-input';
|
||||||
|
const pm_strip_props = [
|
||||||
|
'background',
|
||||||
|
'background-color',
|
||||||
|
'background-image',
|
||||||
|
'color'
|
||||||
|
];
|
||||||
|
|
||||||
|
const pm_observed_forms = new WeakSet();
|
||||||
|
|
||||||
|
function pm_strip_inline(el) {
|
||||||
|
if (!el || el.nodeType !== 1 || !el.matches?.(pm_input_selector)) return;
|
||||||
|
|
||||||
|
let did_change = false;
|
||||||
|
for (const prop of pm_strip_props) {
|
||||||
|
if (el.style.getPropertyValue(prop)) {
|
||||||
|
el.style.removeProperty(prop);
|
||||||
|
did_change = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (did_change && el.style.length === 0) {
|
||||||
|
el.removeAttribute('style');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pm_attach_observer(form) {
|
||||||
|
if (pm_observed_forms.has(form)) return;
|
||||||
|
pm_observed_forms.add(form);
|
||||||
|
|
||||||
|
form.querySelectorAll(pm_input_selector).forEach(pm_strip_inline);
|
||||||
|
|
||||||
|
const pm_mo = new MutationObserver(mutations => {
|
||||||
|
for (const m of mutations) {
|
||||||
|
if (m.type === 'attributes') {
|
||||||
|
pm_strip_inline(m.target);
|
||||||
|
} else if (m.type === 'childList') {
|
||||||
|
for (const n of m.addedNodes) {
|
||||||
|
if (n.nodeType !== 1) continue;
|
||||||
|
if (n.matches?.(pm_input_selector)) pm_strip_inline(n);
|
||||||
|
n.querySelectorAll?.(pm_input_selector).forEach(pm_strip_inline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pm_mo.observe(form, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['style'],
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pm_init() {
|
||||||
|
document.querySelectorAll('form.ant-form').forEach(pm_attach_observer);
|
||||||
|
const pm_host = document.getElementById('login') || document.body;
|
||||||
|
const pm_wait_for_forms = new MutationObserver(mutations => {
|
||||||
|
for (const m of mutations) {
|
||||||
|
for (const n of m.addedNodes) {
|
||||||
|
if (n.nodeType !== 1) continue;
|
||||||
|
if (n.matches?.('form.ant-form')) pm_attach_observer(n);
|
||||||
|
n.querySelectorAll?.('form.ant-form').forEach(pm_attach_observer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pm_wait_for_forms.observe(pm_host, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', pm_init, { once: true });
|
||||||
|
} else {
|
||||||
|
pm_init();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{{ template "page/body_end" .}}
|
{{ template "page/body_end" .}}
|
||||||
165
web/html/servers.html
Normal file
165
web/html/servers.html
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
{{template "header" .}}
|
||||||
|
|
||||||
|
<div id="app" class="row" v-cloak>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Server Management</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button class="btn btn-primary" @click="showAddModal">Add Server</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Address</th>
|
||||||
|
<th>Port</th>
|
||||||
|
<th>Enabled</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(server, index) in servers">
|
||||||
|
<td>{{index + 1}}</td>
|
||||||
|
<td>{{server.name}}</td>
|
||||||
|
<td>{{server.address}}</td>
|
||||||
|
<td>{{server.port}}</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="server.enable" class="badge bg-success">Yes</span>
|
||||||
|
<span v-else class="badge bg-danger">No</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-info btn-sm" @click="showEditModal(server)">Edit</button>
|
||||||
|
<button class="btn btn-danger btn-sm" @click="deleteServer(server.id)">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add/Edit Modal -->
|
||||||
|
<div class="modal fade" id="serverModal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">{{modal.title}}</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Name</label>
|
||||||
|
<input type="text" class="form-control" v-model="modal.server.name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Address (IP or Domain)</label>
|
||||||
|
<input type="text" class="form-control" v-model="modal.server.address">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Port</label>
|
||||||
|
<input type="number" class="form-control" v-model.number="modal.server.port">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>API Key</label>
|
||||||
|
<input type="text" class="form-control" v-model="modal.server.apiKey">
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" v-model="modal.server.enable">
|
||||||
|
<label class="form-check-label">Enabled</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="saveServer">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const app = new Vue({
|
||||||
|
el: '#app',
|
||||||
|
data: {
|
||||||
|
servers: [],
|
||||||
|
modal: {
|
||||||
|
title: '',
|
||||||
|
server: {
|
||||||
|
name: '',
|
||||||
|
address: '',
|
||||||
|
port: 0,
|
||||||
|
apiKey: '',
|
||||||
|
enable: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadServers() {
|
||||||
|
axios.get('{{.base_path}}server/list')
|
||||||
|
.then(response => {
|
||||||
|
this.servers = response.data.obj;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert(error.response.data.msg);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showAddModal() {
|
||||||
|
this.modal.title = 'Add Server';
|
||||||
|
this.modal.server = {
|
||||||
|
name: '',
|
||||||
|
address: '',
|
||||||
|
port: 0,
|
||||||
|
apiKey: '',
|
||||||
|
enable: true
|
||||||
|
};
|
||||||
|
$('#serverModal').modal('show');
|
||||||
|
},
|
||||||
|
showEditModal(server) {
|
||||||
|
this.modal.title = 'Edit Server';
|
||||||
|
this.modal.server = Object.assign({}, server);
|
||||||
|
$('#serverModal').modal('show');
|
||||||
|
},
|
||||||
|
saveServer() {
|
||||||
|
let url = '{{.base_path}}server/add';
|
||||||
|
if (this.modal.server.id) {
|
||||||
|
url = `{{.base_path}}server/update/${this.modal.server.id}`;
|
||||||
|
}
|
||||||
|
axios.post(url, this.modal.server)
|
||||||
|
.then(response => {
|
||||||
|
alert(response.data.msg);
|
||||||
|
$('#serverModal').modal('hide');
|
||||||
|
this.loadServers();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert(error.response.data.msg);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteServer(id) {
|
||||||
|
if (!confirm('Are you sure you want to delete this server?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
axios.post(`{{.base_path}}server/del/${id}`)
|
||||||
|
.then(response => {
|
||||||
|
alert(response.data.msg);
|
||||||
|
this.loadServers();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert(error.response.data.msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadServers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{template "footer" .}}
|
||||||
34
web/middleware/auth.go
Normal file
34
web/middleware/auth.go
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"x-ui/web/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ApiAuth() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
apiKey := c.GetHeader("Api-Key")
|
||||||
|
if apiKey == "" {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "API key is required"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settingService := service.SettingService{}
|
||||||
|
panelAPIKey, err := settingService.GetAPIKey()
|
||||||
|
if err != nil || panelAPIKey == "" {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "API key not configured on the panel"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiKey != panelAPIKey {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid API key"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -617,6 +620,11 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
||||||
}
|
}
|
||||||
s.xrayApi.Close()
|
s.xrayApi.Close()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
body, _ := json.Marshal(data)
|
||||||
|
s.syncWithSlaves("POST", "/panel/inbound/api/addClient", bytes.NewReader(body))
|
||||||
|
}
|
||||||
|
|
||||||
return needRestart, tx.Save(oldInbound).Error
|
return needRestart, tx.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -705,6 +713,11 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
|
||||||
s.xrayApi.Close()
|
s.xrayApi.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
s.syncWithSlaves("POST", fmt.Sprintf("/panel/inbound/api/%d/delClient/%s", inboundId, clientId), nil)
|
||||||
|
}
|
||||||
|
|
||||||
return needRestart, db.Save(oldInbound).Error
|
return needRestart, db.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -880,6 +893,12 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||||
logger.Debug("Client old email not found")
|
logger.Debug("Client old email not found")
|
||||||
needRestart = true
|
needRestart = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
body, _ := json.Marshal(data)
|
||||||
|
s.syncWithSlaves("POST", fmt.Sprintf("/panel/inbound/api/updateClient/%s", clientId), bytes.NewReader(body))
|
||||||
|
}
|
||||||
|
|
||||||
return needRestart, tx.Save(oldInbound).Error
|
return needRestart, tx.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2293,6 +2312,44 @@ func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, [
|
||||||
|
|
||||||
return validEmails, extraEmails, nil
|
return validEmails, extraEmails, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) syncWithSlaves(method string, path string, body io.Reader) {
|
||||||
|
serverService := MultiServerService{}
|
||||||
|
servers, err := serverService.GetServers()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Failed to get servers for syncing:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, server := range servers {
|
||||||
|
if !server.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("http://%s:%d%s", server.Address, server.Port, path)
|
||||||
|
req, err := http.NewRequest(method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("Failed to create request for server %s: %v", server.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Api-Key", server.APIKey)
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("Failed to send request to server %s: %v", server.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
logger.Warningf("Failed to sync with server %s. Status: %s, Body: %s", server.Name, resp.Status, string(bodyBytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (bool, error) {
|
func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (bool, error) {
|
||||||
oldInbound, err := s.GetInbound(inboundId)
|
oldInbound, err := s.GetInbound(inboundId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -2384,4 +2441,5 @@ func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (b
|
||||||
}
|
}
|
||||||
|
|
||||||
return needRestart, db.Save(oldInbound).Error
|
return needRestart, db.Save(oldInbound).Error
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
72
web/service/inbound_service_sync_test.go
Normal file
72
web/service/inbound_service_sync_test.go
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"x-ui/database"
|
||||||
|
"x-ui/database/model"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInboundServiceSync(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
// Mock server to simulate a slave
|
||||||
|
var receivedApiKey string
|
||||||
|
var receivedBody []byte
|
||||||
|
mockSlave := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
receivedApiKey = r.Header.Get("Api-Key")
|
||||||
|
receivedBody, _ = io.ReadAll(r.Body)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer mockSlave.Close()
|
||||||
|
|
||||||
|
// Add the mock slave to the database
|
||||||
|
multiServerService := MultiServerService{}
|
||||||
|
mockSlaveURL, _ := url.Parse(mockSlave.URL)
|
||||||
|
mockSlavePort, _ := strconv.Atoi(mockSlaveURL.Port())
|
||||||
|
slaveServer := &model.Server{
|
||||||
|
Name: "mock-slave",
|
||||||
|
Address: mockSlaveURL.Hostname(),
|
||||||
|
Port: mockSlavePort,
|
||||||
|
APIKey: "slave-api-key",
|
||||||
|
Enable: true,
|
||||||
|
}
|
||||||
|
multiServerService.AddServer(slaveServer)
|
||||||
|
|
||||||
|
// Create a test inbound and client
|
||||||
|
inboundService := InboundService{}
|
||||||
|
db := database.GetDB()
|
||||||
|
testInbound := &model.Inbound{
|
||||||
|
UserId: 1,
|
||||||
|
Remark: "test-inbound",
|
||||||
|
Enable: true,
|
||||||
|
Settings: `{"clients":[]}`,
|
||||||
|
}
|
||||||
|
db.Create(testInbound)
|
||||||
|
|
||||||
|
clientData := model.Client{
|
||||||
|
Email: "test@example.com",
|
||||||
|
ID: "test-id",
|
||||||
|
}
|
||||||
|
clientBytes, _ := json.Marshal([]model.Client{clientData})
|
||||||
|
inboundData := &model.Inbound{
|
||||||
|
Id: testInbound.Id,
|
||||||
|
Settings: string(clientBytes),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test AddInboundClient sync
|
||||||
|
inboundService.AddInboundClient(inboundData)
|
||||||
|
|
||||||
|
assert.Equal(t, "slave-api-key", receivedApiKey)
|
||||||
|
var receivedInbound model.Inbound
|
||||||
|
json.Unmarshal(receivedBody, &receivedInbound)
|
||||||
|
assert.Equal(t, 1, receivedInbound.Id)
|
||||||
|
}
|
||||||
37
web/service/multi_server_service.go
Normal file
37
web/service/multi_server_service.go
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"x-ui/database"
|
||||||
|
"x-ui/database/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MultiServerService struct{}
|
||||||
|
|
||||||
|
func (s *MultiServerService) GetServers() ([]*model.Server, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var servers []*model.Server
|
||||||
|
err := db.Find(&servers).Error
|
||||||
|
return servers, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiServerService) GetServer(id int) (*model.Server, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var server model.Server
|
||||||
|
err := db.First(&server, id).Error
|
||||||
|
return &server, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiServerService) AddServer(server *model.Server) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
return db.Create(server).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiServerService) UpdateServer(server *model.Server) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
return db.Save(server).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiServerService) DeleteServer(id int) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
return db.Delete(&model.Server{}, id).Error
|
||||||
|
}
|
||||||
63
web/service/multi_server_service_test.go
Normal file
63
web/service/multi_server_service_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"x-ui/database"
|
||||||
|
"x-ui/database/model"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setup() {
|
||||||
|
dbPath := "test.db"
|
||||||
|
os.Remove(dbPath)
|
||||||
|
database.InitDB(dbPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func teardown() {
|
||||||
|
db, _ := database.GetDB().DB()
|
||||||
|
db.Close()
|
||||||
|
os.Remove("test.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultiServerService(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
service := MultiServerService{}
|
||||||
|
|
||||||
|
// Test AddServer
|
||||||
|
server := &model.Server{
|
||||||
|
Name: "test-server",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
Port: 54321,
|
||||||
|
APIKey: "test-key",
|
||||||
|
Enable: true,
|
||||||
|
}
|
||||||
|
err := service.AddServer(server)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test GetServer
|
||||||
|
retrievedServer, err := service.GetServer(server.Id)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, server.Name, retrievedServer.Name)
|
||||||
|
|
||||||
|
// Test GetServers
|
||||||
|
servers, err := service.GetServers()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, servers, 1)
|
||||||
|
|
||||||
|
// Test UpdateServer
|
||||||
|
retrievedServer.Name = "updated-server"
|
||||||
|
err = service.UpdateServer(retrievedServer)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
updatedServer, _ := service.GetServer(server.Id)
|
||||||
|
assert.Equal(t, "updated-server", updatedServer.Name)
|
||||||
|
|
||||||
|
// Test DeleteServer
|
||||||
|
err = service.DeleteServer(server.Id)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = service.GetServer(server.Id)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
@ -174,6 +174,16 @@ type CPUSample struct {
|
||||||
Cpu float64 `json:"cpu"` // percent 0..100
|
Cpu float64 `json:"cpu"` // percent 0..100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LogEntry struct {
|
||||||
|
DateTime time.Time
|
||||||
|
FromAddress string
|
||||||
|
ToAddress string
|
||||||
|
Inbound string
|
||||||
|
Outbound string
|
||||||
|
Email string
|
||||||
|
Event int
|
||||||
|
}
|
||||||
|
|
||||||
func getPublicIP(url string) string {
|
func getPublicIP(url string) string {
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: 3 * time.Second,
|
Timeout: 3 * time.Second,
|
||||||
|
|
@ -704,19 +714,25 @@ func (s *ServerService) GetXrayLogs(
|
||||||
showBlocked string,
|
showBlocked string,
|
||||||
showProxy string,
|
showProxy string,
|
||||||
freedoms []string,
|
freedoms []string,
|
||||||
blackholes []string) []string {
|
blackholes []string) []LogEntry {
|
||||||
|
|
||||||
|
const (
|
||||||
|
Direct = iota
|
||||||
|
Blocked
|
||||||
|
Proxied
|
||||||
|
)
|
||||||
|
|
||||||
countInt, _ := strconv.Atoi(count)
|
countInt, _ := strconv.Atoi(count)
|
||||||
var lines []string
|
var entries []LogEntry
|
||||||
|
|
||||||
pathToAccessLog, err := xray.GetAccessLogPath()
|
pathToAccessLog, err := xray.GetAccessLogPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return lines
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(pathToAccessLog)
|
file, err := os.Open(pathToAccessLog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return lines
|
return nil
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
|
|
@ -735,37 +751,62 @@ func (s *ServerService) GetXrayLogs(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
//adding suffixes to further distinguish entries by outbound
|
var entry LogEntry
|
||||||
if hasSuffix(line, freedoms) {
|
parts := strings.Fields(line)
|
||||||
|
|
||||||
|
for i, part := range parts {
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
dateTime, err := time.Parse("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry.DateTime = dateTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if part == "from" {
|
||||||
|
entry.FromAddress = parts[i+1]
|
||||||
|
} else if part == "accepted" {
|
||||||
|
entry.ToAddress = parts[i+1]
|
||||||
|
} else if strings.HasPrefix(part, "[") {
|
||||||
|
entry.Inbound = part[1:]
|
||||||
|
} else if strings.HasSuffix(part, "]") {
|
||||||
|
entry.Outbound = part[:len(part)-1]
|
||||||
|
} else if part == "email:" {
|
||||||
|
entry.Email = parts[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if logEntryContains(line, freedoms) {
|
||||||
if showDirect == "false" {
|
if showDirect == "false" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
line = line + " f"
|
entry.Event = Direct
|
||||||
} else if hasSuffix(line, blackholes) {
|
} else if logEntryContains(line, blackholes) {
|
||||||
if showBlocked == "false" {
|
if showBlocked == "false" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
line = line + " b"
|
entry.Event = Blocked
|
||||||
} else {
|
} else {
|
||||||
if showProxy == "false" {
|
if showProxy == "false" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
line = line + " p"
|
entry.Event = Proxied
|
||||||
}
|
}
|
||||||
|
|
||||||
lines = append(lines, line)
|
entries = append(entries, entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(lines) > countInt {
|
if len(entries) > countInt {
|
||||||
lines = lines[len(lines)-countInt:]
|
entries = entries[len(entries)-countInt:]
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasSuffix(line string, suffixes []string) bool {
|
func logEntryContains(line string, suffixes []string) bool {
|
||||||
for _, sfx := range suffixes {
|
for _, sfx := range suffixes {
|
||||||
if strings.HasSuffix(line, sfx+"]") {
|
if strings.Contains(line, sfx+"]") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,21 @@ func (s *SettingService) getSetting(key string) (*model.Setting, error) {
|
||||||
return setting, nil
|
return setting, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetAPIKey() (string, error) {
|
||||||
|
setting, err := s.getSetting("ApiKey")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if setting == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return setting.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetAPIKey(apiKey string) error {
|
||||||
|
return s.saveSetting("ApiKey", apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingService) saveSetting(key string, value string) error {
|
func (s *SettingService) saveSetting(key string, value string) error {
|
||||||
setting, err := s.getSetting(key)
|
setting, err := s.getSetting(key)
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
g := engine.Group(basePath)
|
g := engine.Group(basePath)
|
||||||
|
|
||||||
s.index = controller.NewIndexController(g)
|
s.index = controller.NewIndexController(g)
|
||||||
s.server = controller.NewServerController(g)
|
s.server = controller.NewMultiServerController(g)
|
||||||
s.panel = controller.NewXUIController(g)
|
s.panel = controller.NewXUIController(g)
|
||||||
s.api = controller.NewAPIController(g)
|
s.api = controller.NewAPIController(g)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue