diff --git a/database/db.go b/database/db.go index c72d28cf..484f1758 100644 --- a/database/db.go +++ b/database/db.go @@ -35,6 +35,7 @@ func initModels() error { &model.InboundClientIps{}, &xray.ClientTraffic{}, &model.HistoryOfSeeders{}, + &model.Server{}, } for _, model := range models { if err := db.AutoMigrate(model); err != nil { diff --git a/database/model/model.go b/database/model/model.go index 2e7095d3..68d56593 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -105,3 +105,12 @@ type Client struct { Comment string `json:"comment" form:"comment"` Reset int `json:"reset" form:"reset"` } + +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"` +} diff --git a/go.mod b/go.mod index 32b6856c..4d18f4b8 100644 --- a/go.mod +++ b/go.mod @@ -61,6 +61,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // 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/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect diff --git a/install.sh b/install.sh index d3e6dd1b..5b47fd55 100644 --- a/install.sh +++ b/install.sh @@ -130,6 +130,13 @@ config_after_install() { fi /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() { diff --git a/main.go b/main.go index 9986ede1..83739ebf 100644 --- a/main.go +++ b/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()) if err != nil { fmt.Println("Database initialization failed:", err) @@ -242,6 +242,15 @@ func updateSetting(port int, username string, password string, webBasePath strin settingService := service.SettingService{} 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 { err := settingService.SetPort(port) if err != nil { @@ -388,9 +397,11 @@ func main() { var show bool var getCert bool var resetTwoFactor bool + var apiKey string settingCmd.BoolVar(&reset, "reset", false, "Reset all settings") settingCmd.BoolVar(&show, "show", false, "Display current settings") 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(&password, "password", "", "Set login password") settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel") @@ -440,7 +451,7 @@ func main() { if reset { resetSetting() } else { - updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor) + updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor, apiKey) } if show { showSetting(show) diff --git a/sub/subService.go b/sub/subService.go index dfb0863e..81d0f9fb 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -154,26 +154,43 @@ func (s *SubService) getFallbackMaster(dest string, streamSettings string) (stri } func (s *SubService) getLink(inbound *model.Inbound, email string) string { - switch inbound.Protocol { - case "vmess": - return s.genVmessLink(inbound, email) - case "vless": - return s.genVlessLink(inbound, email) - case "trojan": - return s.genTrojanLink(inbound, email) - case "shadowsocks": - return s.genShadowsocksLink(inbound, email) + serverService := service.MultiServerService{} + servers, err := serverService.GetServers() + if err != nil { + logger.Warning("Failed to get servers for subscription:", err) + return "" } - return "" + + 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) string { +func (s *SubService) genVmessLink(inbound *model.Inbound, email string, server *model.Server) string { if inbound.Protocol != model.VMESS { return "" } obj := map[string]any{ "v": "2", - "add": s.address, + "add": server.Address, "port": inbound.Port, "type": "none", } @@ -286,7 +303,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { 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["port"] = int(ep["port"].(float64)) @@ -302,14 +319,14 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { return links } - obj["ps"] = s.genRemark(inbound, email, "") + obj["ps"] = s.genRemark(inbound, email, "", server.Name) jsonStr, _ := json.MarshalIndent(obj, "", " ") return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) } -func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { - address := s.address +func (s *SubService) genVlessLink(inbound *model.Inbound, email string, server *model.Server) string { + address := server.Address if inbound.Protocol != model.VLESS { return "" } @@ -482,7 +499,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { // Set the new query values on the URL 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 { links += "\n" @@ -503,12 +520,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { // Set the new query values on the URL url.RawQuery = q.Encode() - url.Fragment = s.genRemark(inbound, email, "") + url.Fragment = s.genRemark(inbound, email, "", server.Name) return url.String() } -func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string { - address := s.address +func (s *SubService) genTrojanLink(inbound *model.Inbound, email string, server *model.Server) string { + address := server.Address if inbound.Protocol != model.Trojan { return "" } @@ -677,7 +694,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string // Set the new query values on the URL 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 { links += "\n" @@ -699,12 +716,12 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string // Set the new query values on the URL url.RawQuery = q.Encode() - url.Fragment = s.genRemark(inbound, email, "") + url.Fragment = s.genRemark(inbound, email, "", server.Name) return url.String() } -func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string { - address := s.address +func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, server *model.Server) string { + address := server.Address if inbound.Protocol != model.Shadowsocks { return "" } @@ -844,7 +861,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st // Set the new query values on the URL 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 { links += "\n" @@ -865,17 +882,18 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st // Set the new query values on the URL url.RawQuery = q.Encode() - url.Fragment = s.genRemark(inbound, email, "") + url.Fragment = s.genRemark(inbound, email, "", server.Name) 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]) orderChars := s.remarkModel[1:] orders := map[byte]string{ 'i': "", 'e': "", 'o': "", + 's': "", } if len(email) > 0 { orders['e'] = email @@ -886,6 +904,9 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin if len(extra) > 0 { orders['o'] = extra } + if len(serverName) > 0 { + orders['s'] = serverName + } var remark []string for i := 0; i < len(orderChars); i++ { diff --git a/web/controller/inbound.go b/web/controller/inbound.go index 851b4b6f..2566edab 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -6,6 +6,7 @@ import ( "strconv" "x-ui/database/model" + "x-ui/web/middleware" "x-ui/web/service" "x-ui/web/session" @@ -32,15 +33,26 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) { g.POST("/update/:id", a.updateInbound) g.POST("/clientIps/:email", a.getClientIps) g.POST("/clearClientIps/:email", a.clearClientIps) - g.POST("/addClient", a.addInboundClient) - g.POST("/:id/delClient/:clientId", a.delInboundClient) - g.POST("/updateClient/:clientId", a.updateInboundClient) g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic) g.POST("/resetAllTraffics", a.resetAllTraffics) g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics) g.POST("/delDepletedClients/:id", a.delDepletedClients) g.POST("/import", a.importInbound) g.POST("/onlines", a.onlines) + + // Routes for UI + g.POST("/addClient", a.addInboundClient) + g.POST("/:id/delClient/:clientId", a.delInboundClient) + g.POST("/updateClient/:clientId", a.updateInboundClient) + + // Routes for API (for slave servers) + apiGroup := g.Group("/api") + apiGroup.Use(middleware.ApiAuth()) + { + apiGroup.POST("/addClient", a.addInboundClient) + apiGroup.POST("/:id/delClient/:clientId", a.delInboundClient) + apiGroup.POST("/updateClient/:clientId", a.updateInboundClient) + } } func (a *InboundController) getInbounds(c *gin.Context) { diff --git a/web/controller/multi_server_controller.go b/web/controller/multi_server_controller.go new file mode 100644 index 00000000..65605f4a --- /dev/null +++ b/web/controller/multi_server_controller.go @@ -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) +} diff --git a/web/controller/xui.go b/web/controller/xui.go index 5b4c0a18..42c02a15 100644 --- a/web/controller/xui.go +++ b/web/controller/xui.go @@ -24,6 +24,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) { g.GET("/", a.index) g.GET("/inbounds", a.inbounds) + g.GET("/servers", a.servers) g.GET("/settings", a.settings) g.GET("/xray", a.xraySettings) @@ -47,3 +48,7 @@ func (a *XUIController) settings(c *gin.Context) { func (a *XUIController) xraySettings(c *gin.Context) { html(c, "xray.html", "pages.xray.title", nil) } + +func (a *XUIController) servers(c *gin.Context) { + html(c, "servers.html", "Servers", nil) +} diff --git a/web/html/component/aSidebar.html b/web/html/component/aSidebar.html index b69c8f3f..7f0a49a7 100644 --- a/web/html/component/aSidebar.html +++ b/web/html/component/aSidebar.html @@ -54,6 +54,11 @@ icon: 'user', title: '{{ i18n "menu.inbounds"}}' }, + { + key: '{{ .base_path }}panel/servers', + icon: 'cloud-server', + title: 'Servers' + }, { key: '{{ .base_path }}panel/settings', icon: 'setting', diff --git a/web/html/servers.html b/web/html/servers.html new file mode 100644 index 00000000..30354731 --- /dev/null +++ b/web/html/servers.html @@ -0,0 +1,165 @@ +{{template "header" .}} + +
# | +Name | +Address | +Port | +Enabled | +Actions | +
---|---|---|---|---|---|
{{index + 1}} | +{{server.name}} | +{{server.address}} | +{{server.port}} | ++ Yes + No + | ++ + + | +