package controller import ( "fmt" "net/http" "regexp" "strconv" "time" "x-ui/web/global" "x-ui/web/service" "github.com/gin-gonic/gin" ) var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`) type ServerController struct { BaseController serverService service.ServerService settingService service.SettingService lastStatus *service.Status lastVersions []string lastGetVersionsTime int64 // unix seconds } func NewServerController(g *gin.RouterGroup) *ServerController { a := &ServerController{} a.initRouter(g) a.startTask() return a } func (a *ServerController) initRouter(g *gin.RouterGroup) { g.GET("/status", a.status) g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket) g.GET("/getXrayVersion", a.getXrayVersion) g.GET("/getConfigJson", a.getConfigJson) g.GET("/getDb", a.getDb) g.GET("/getNewUUID", a.getNewUUID) g.GET("/getNewX25519Cert", a.getNewX25519Cert) g.GET("/getNewmldsa65", a.getNewmldsa65) g.GET("/getNewmlkem768", a.getNewmlkem768) g.GET("/getNewVlessEnc", a.getNewVlessEnc) g.POST("/stopXrayService", a.stopXrayService) g.POST("/restartXrayService", a.restartXrayService) g.POST("/installXray/:version", a.installXray) g.POST("/updateGeofile", a.updateGeofile) g.POST("/updateGeofile/:fileName", a.updateGeofile) g.POST("/logs/:count", a.getLogs) g.POST("/xraylogs/:count", a.getXrayLogs) g.POST("/importDB", a.importDB) g.POST("/getNewEchCert", a.getNewEchCert) } func (a *ServerController) refreshStatus() { a.lastStatus = a.serverService.GetStatus(a.lastStatus) // collect cpu history when status is fresh if a.lastStatus != nil { a.serverService.AppendCpuSample(time.Now(), a.lastStatus.Cpu) } } func (a *ServerController) startTask() { webServer := global.GetWebServer() c := webServer.GetCron() c.AddFunc("@every 2s", func() { // Always refresh to keep CPU history collected continuously. // Sampling is lightweight and capped to ~6 hours in memory. a.refreshStatus() }) } func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) } func (a *ServerController) getCpuHistoryBucket(c *gin.Context) { bucketStr := c.Param("bucket") bucket, err := strconv.Atoi(bucketStr) if err != nil || bucket <= 0 { jsonMsg(c, "invalid bucket", fmt.Errorf("bad bucket")) return } allowed := map[int]bool{ 2: true, // Real-time view 30: true, // 30s intervals 60: true, // 1m intervals 120: true, // 2m intervals 180: true, // 3m intervals 300: true, // 5m intervals } if !allowed[bucket] { jsonMsg(c, "invalid bucket", fmt.Errorf("unsupported bucket")) return } points := a.serverService.AggregateCpuHistory(bucket, 60) jsonObj(c, points, nil) } func (a *ServerController) getXrayVersion(c *gin.Context) { now := time.Now().Unix() if now-a.lastGetVersionsTime <= 60 { // 1 minute cache jsonObj(c, a.lastVersions, nil) return } versions, err := a.serverService.GetXrayVersions() if err != nil { jsonMsg(c, I18nWeb(c, "getVersion"), err) return } a.lastVersions = versions a.lastGetVersionsTime = now jsonObj(c, versions, nil) } func (a *ServerController) installXray(c *gin.Context) { version := c.Param("version") err := a.serverService.UpdateXray(version) jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err) } func (a *ServerController) updateGeofile(c *gin.Context) { fileName := c.Param("fileName") err := a.serverService.UpdateGeofile(fileName) jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err) } func (a *ServerController) stopXrayService(c *gin.Context) { err := a.serverService.StopXrayService() if err != nil { jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err) return } jsonMsg(c, I18nWeb(c, "pages.xray.stopSuccess"), err) } func (a *ServerController) restartXrayService(c *gin.Context) { err := a.serverService.RestartXrayService() if err != nil { jsonMsg(c, I18nWeb(c, "pages.xray.restartError"), err) return } jsonMsg(c, I18nWeb(c, "pages.xray.restartSuccess"), err) } func (a *ServerController) getLogs(c *gin.Context) { count := c.Param("count") level := c.PostForm("level") syslog := c.PostForm("syslog") logs := a.serverService.GetLogs(count, level, syslog) jsonObj(c, logs, nil) } func (a *ServerController) getXrayLogs(c *gin.Context) { count := c.Param("count") filter := c.PostForm("filter") showDirect := c.PostForm("showDirect") showBlocked := c.PostForm("showBlocked") showProxy := c.PostForm("showProxy") var freedoms []string var blackholes []string //getting tags for freedom and blackhole outbounds config, err := a.settingService.GetDefaultXrayConfig() if err == nil && config != nil { if cfgMap, ok := config.(map[string]interface{}); ok { if outbounds, ok := cfgMap["outbounds"].([]interface{}); ok { for _, outbound := range outbounds { if obMap, ok := outbound.(map[string]interface{}); ok { switch obMap["protocol"] { case "freedom": if tag, ok := obMap["tag"].(string); ok { freedoms = append(freedoms, tag) } case "blackhole": if tag, ok := obMap["tag"].(string); ok { blackholes = append(blackholes, tag) } } } } } } } if len(freedoms) == 0 { freedoms = []string{"direct"} } if len(blackholes) == 0 { blackholes = []string{"blocked"} } logs := a.serverService.GetXrayLogs(count, filter, showDirect, showBlocked, showProxy, freedoms, blackholes) jsonObj(c, logs, nil) } func (a *ServerController) getConfigJson(c *gin.Context) { configJson, err := a.serverService.GetConfigJson() if err != nil { jsonMsg(c, I18nWeb(c, "pages.index.getConfigError"), err) return } jsonObj(c, configJson, nil) } func (a *ServerController) getDb(c *gin.Context) { db, err := a.serverService.GetDb() if err != nil { jsonMsg(c, I18nWeb(c, "pages.index.getDatabaseError"), err) return } filename := "x-ui.db" if !isValidFilename(filename) { c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename")) return } // Set the headers for the response c.Header("Content-Type", "application/octet-stream") c.Header("Content-Disposition", "attachment; filename="+filename) // Write the file contents to the response c.Writer.Write(db) } func isValidFilename(filename string) bool { // Validate that the filename only contains allowed characters return filenameRegex.MatchString(filename) } func (a *ServerController) importDB(c *gin.Context) { // Get the file from the request body file, _, err := c.Request.FormFile("db") if err != nil { jsonMsg(c, I18nWeb(c, "pages.index.readDatabaseError"), err) return } defer file.Close() // Always restart Xray before return defer a.serverService.RestartXrayService() // lastGetStatusTime removed; no longer needed // Import it err = a.serverService.ImportDB(file) if err != nil { jsonMsg(c, I18nWeb(c, "pages.index.importDatabaseError"), err) return } jsonObj(c, I18nWeb(c, "pages.index.importDatabaseSuccess"), nil) } func (a *ServerController) getNewX25519Cert(c *gin.Context) { cert, err := a.serverService.GetNewX25519Cert() if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.getNewX25519CertError"), err) return } jsonObj(c, cert, nil) } func (a *ServerController) getNewmldsa65(c *gin.Context) { cert, err := a.serverService.GetNewmldsa65() if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.getNewmldsa65Error"), err) return } jsonObj(c, cert, nil) } func (a *ServerController) getNewEchCert(c *gin.Context) { sni := c.PostForm("sni") cert, err := a.serverService.GetNewEchCert(sni) if err != nil { jsonMsg(c, "get ech certificate", err) return } jsonObj(c, cert, nil) } func (a *ServerController) getNewVlessEnc(c *gin.Context) { out, err := a.serverService.GetNewVlessEnc() if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.getNewVlessEncError"), err) return } jsonObj(c, out, nil) } func (a *ServerController) getNewUUID(c *gin.Context) { uuidResp, err := a.serverService.GetNewUUID() if err != nil { jsonMsg(c, "Failed to generate UUID", err) return } jsonObj(c, uuidResp, nil) } func (a *ServerController) getNewmlkem768(c *gin.Context) { out, err := a.serverService.GetNewmlkem768() if err != nil { jsonMsg(c, "Failed to generate mlkem768 keys", err) return } jsonObj(c, out, nil) }