mirror of
				https://github.com/MHSanaei/3x-ui.git
				synced 2025-10-30 20:02:51 +00:00 
			
		
		
		
	Merge pull request #11 from serogaq/develop
Features: ClientOnlineIPs Api, WebUI - Access and error logs; GetRemoteIp priorityHeader (env); x-ui/caching; WebUI - XuiLatestVersion
This commit is contained in:
		
						commit
						2ec811d2ad
					
				
					 29 changed files with 522 additions and 112 deletions
				
			
		|  | @ -10,3 +10,4 @@ XUI_VLESS_SNI="" | ||||||
| #XUI_SUB_PROFILE_WEB_PAGE_URL="" | #XUI_SUB_PROFILE_WEB_PAGE_URL="" | ||||||
| #XUI_DEBUG="false" | #XUI_DEBUG="false" | ||||||
| #XUI_LOG_LEVEL="info" | #XUI_LOG_LEVEL="info" | ||||||
|  | #XUI_GETREMOTEIP_PRIORITY_HEADER="" | ||||||
							
								
								
									
										52
									
								
								caching/caching.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								caching/caching.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,52 @@ | ||||||
|  | package caching | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/patrickmn/go-cache" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type Cache struct { | ||||||
|  | 	memoryCache  *cache.Cache | ||||||
|  | 
 | ||||||
|  | 	ctx    context.Context | ||||||
|  | 	cancel context.CancelFunc | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewCache() *Cache { | ||||||
|  | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
|  | 	return &Cache{ | ||||||
|  | 		ctx:    ctx, | ||||||
|  | 		cancel: cancel, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Cache) Init() (err error) { | ||||||
|  | 	defer func() { | ||||||
|  | 		if err != nil { | ||||||
|  | 			s.Flush() | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	s.memoryCache = cache.New(10*time.Minute, 10*time.Minute) | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Cache) Flush() error { | ||||||
|  | 	if s.memoryCache != nil { | ||||||
|  | 		s.memoryCache.Flush() | ||||||
|  | 	} | ||||||
|  | 	s.cancel() | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Cache) GetCtx() context.Context { | ||||||
|  | 	return s.ctx | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Cache) Memory() *cache.Cache { | ||||||
|  | 	return s.memoryCache | ||||||
|  | } | ||||||
|  | @ -51,6 +51,7 @@ services: | ||||||
|       XUI_SUB_PROFILE_TITLE: "${XUI_SUB_PROFILE_TITLE:-}" |       XUI_SUB_PROFILE_TITLE: "${XUI_SUB_PROFILE_TITLE:-}" | ||||||
|       XUI_SUB_SUPPORT_URL: "${XUI_SUB_SUPPORT_URL:-}" |       XUI_SUB_SUPPORT_URL: "${XUI_SUB_SUPPORT_URL:-}" | ||||||
|       XUI_SUB_PROFILE_WEB_PAGE_URL: "${XUI_SUB_PROFILE_WEB_PAGE_URL:-}" |       XUI_SUB_PROFILE_WEB_PAGE_URL: "${XUI_SUB_PROFILE_WEB_PAGE_URL:-}" | ||||||
|  |       XUI_GETREMOTEIP_PRIORITY_HEADER: "${XUI_GETREMOTEIP_PRIORITY_HEADER:-}" | ||||||
|       XUI_DEBUG: "${XUI_DEBUG:-false}" |       XUI_DEBUG: "${XUI_DEBUG:-false}" | ||||||
|       XUI_LOG_LEVEL: "${XUI_LOG_LEVEL:-info}" |       XUI_LOG_LEVEL: "${XUI_LOG_LEVEL:-info}" | ||||||
|     tty: true |     tty: true | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							|  | @ -59,6 +59,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/onsi/ginkgo/v2 v2.22.0 // indirect | 	github.com/onsi/ginkgo/v2 v2.22.0 // indirect | ||||||
|  | 	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect | ||||||
| 	github.com/pires/go-proxyproto v0.8.0 // indirect | 	github.com/pires/go-proxyproto v0.8.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 | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							|  | @ -119,6 +119,8 @@ github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= | ||||||
| github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= | github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= | ||||||
| github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= | ||||||
| github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= | ||||||
|  | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= | ||||||
|  | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= | ||||||
| github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= | ||||||
| github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= | ||||||
| github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= | ||||||
|  |  | ||||||
|  | @ -4,7 +4,6 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"time" | 	"time" | ||||||
| 	"strings" |  | ||||||
| 
 | 
 | ||||||
| 	"github.com/op/go-logging" | 	"github.com/op/go-logging" | ||||||
| ) | ) | ||||||
|  | @ -127,35 +126,3 @@ func GetLogs(c int, level string) []string { | ||||||
| 	} | 	} | ||||||
| 	return output | 	return output | ||||||
| } | } | ||||||
| 
 |  | ||||||
| func GetLogsSniffedDomains(c int) []string { |  | ||||||
| 	var output []string |  | ||||||
| 	logLevel, _ := logging.LogLevel("info") |  | ||||||
| 
 |  | ||||||
| 	for i := len(logBuffer) - 1; i >= 0 && len(output) <= c; i-- { |  | ||||||
| 		if logBuffer[i].level <= logLevel && strings.Contains(logBuffer[i].log, "sniffed domain: ") { |  | ||||||
| 			index := strings.LastIndex(logBuffer[i].log, ": ") |  | ||||||
| 			if index != -1 { |  | ||||||
| 				domain := logBuffer[i].log[index+2:] |  | ||||||
| 				output = append(output, fmt.Sprintf("%s - %s", logBuffer[i].time, domain)) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return output |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func GetLogsBlockedDomains(c int) []string { |  | ||||||
| 	var output []string |  | ||||||
| 	logLevel, _ := logging.LogLevel("info") |  | ||||||
| 
 |  | ||||||
| 	for i := len(logBuffer) - 1; i >= 0 && len(output) <= c; i-- { |  | ||||||
| 		if logBuffer[i].level <= logLevel && strings.Contains(logBuffer[i].log, "[blocked] for ") { |  | ||||||
| 			index := strings.LastIndex(logBuffer[i].log, "for [") |  | ||||||
| 			if index != -1 { |  | ||||||
| 				domain := strings.Replace(logBuffer[i].log[index+5:], "]", "", -1) |  | ||||||
| 				output = append(output, fmt.Sprintf("%s - %s", logBuffer[i].time, domain)) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return output |  | ||||||
| } |  | ||||||
|  |  | ||||||
							
								
								
									
										28
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								main.go
									
									
									
									
									
								
							|  | @ -11,6 +11,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"x-ui/config" | 	"x-ui/config" | ||||||
| 	"x-ui/database" | 	"x-ui/database" | ||||||
|  | 	"x-ui/caching" | ||||||
| 	"x-ui/logger" | 	"x-ui/logger" | ||||||
| 	"x-ui/sub" | 	"x-ui/sub" | ||||||
| 	"x-ui/web" | 	"x-ui/web" | ||||||
|  | @ -61,6 +62,17 @@ func runWebServer() { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	var cacheInstance *caching.Cache | ||||||
|  | 	cacheInstance = caching.NewCache() | ||||||
|  | 	global.SetCache(cacheInstance) | ||||||
|  | 	err = cacheInstance.Init() | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatalf("Cache initialization error: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} else { | ||||||
|  | 		log.Println("Cache initialized") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	sigCh := make(chan os.Signal, 1) | 	sigCh := make(chan os.Signal, 1) | ||||||
| 	// Trap shutdown signals
 | 	// Trap shutdown signals
 | ||||||
| 	signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM) | 	signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM) | ||||||
|  | @ -79,6 +91,10 @@ func runWebServer() { | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logger.Debug("Error stopping sub server:", err) | 				logger.Debug("Error stopping sub server:", err) | ||||||
| 			} | 			} | ||||||
|  | 			err = cacheInstance.Flush() | ||||||
|  | 			if err != nil { | ||||||
|  | 				logger.Debug("Error clearing cache:", err) | ||||||
|  | 			} | ||||||
| 
 | 
 | ||||||
| 			server = web.NewServer() | 			server = web.NewServer() | ||||||
| 			global.SetWebServer(server) | 			global.SetWebServer(server) | ||||||
|  | @ -98,10 +114,20 @@ func runWebServer() { | ||||||
| 			} | 			} | ||||||
| 			log.Println("Sub server restarted successfully.") | 			log.Println("Sub server restarted successfully.") | ||||||
| 
 | 
 | ||||||
|  | 			cacheInstance = caching.NewCache() | ||||||
|  | 			global.SetCache(cacheInstance) | ||||||
|  | 			err = cacheInstance.Init() | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Fatalf("Cache re-initialization error: %v", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			log.Println("Cache cleared.") | ||||||
|  | 
 | ||||||
| 		default: | 		default: | ||||||
| 			server.Stop() | 			server.Stop() | ||||||
| 			subServer.Stop() | 			subServer.Stop() | ||||||
| 			log.Println("Shutting down servers.") | 			cacheInstance.Flush() | ||||||
|  | 			log.Println("Shutting down servers and cache clearing.") | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -12,6 +12,10 @@ import ( | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | type ClientOnlineIPsResponse struct { | ||||||
|  | 	Count int `json:"count"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
| type InboundController struct { | type InboundController struct { | ||||||
| 	inboundService service.InboundService | 	inboundService service.InboundService | ||||||
| 	xrayService    service.XrayService | 	xrayService    service.XrayService | ||||||
|  | @ -30,6 +34,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) { | ||||||
| 	g.POST("/add", a.addInbound) | 	g.POST("/add", a.addInbound) | ||||||
| 	g.POST("/del/:id", a.delInbound) | 	g.POST("/del/:id", a.delInbound) | ||||||
| 	g.POST("/update/:id", a.updateInbound) | 	g.POST("/update/:id", a.updateInbound) | ||||||
|  | 	g.POST("/clientOnlineIps/:email", a.getClientOnlineIPs) | ||||||
| 	g.POST("/clientIps/:email", a.getClientIps) | 	g.POST("/clientIps/:email", a.getClientIps) | ||||||
| 	g.POST("/clearClientIps/:email", a.clearClientIps) | 	g.POST("/clearClientIps/:email", a.clearClientIps) | ||||||
| 	g.POST("/addClient", a.addInboundClient) | 	g.POST("/addClient", a.addInboundClient) | ||||||
|  | @ -146,6 +151,21 @@ func (a *InboundController) updateInbound(c *gin.Context) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (a *InboundController) getClientOnlineIPs(c *gin.Context) { | ||||||
|  | 	email := c.Param("email") | ||||||
|  | 
 | ||||||
|  | 	count, err := a.inboundService.GetClientOnlineIPs(email) | ||||||
|  | 	res := &ClientOnlineIPsResponse{ | ||||||
|  | 		Count: count, | ||||||
|  | 	} | ||||||
|  | 	if err != nil { | ||||||
|  | 		jsonObj(c, res, err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	jsonObj(c, res, nil) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (a *InboundController) getClientIps(c *gin.Context) { | func (a *InboundController) getClientIps(c *gin.Context) { | ||||||
| 	email := c.Param("email") | 	email := c.Param("email") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -45,8 +45,8 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) { | ||||||
| 	g.POST("/restartXrayService", a.restartXrayService) | 	g.POST("/restartXrayService", a.restartXrayService) | ||||||
| 	g.POST("/installXray/:version", a.installXray) | 	g.POST("/installXray/:version", a.installXray) | ||||||
| 	g.POST("/logs/:count", a.getLogs) | 	g.POST("/logs/:count", a.getLogs) | ||||||
| 	g.GET("/logs-sniffed/:count", a.getLogsSniffedDomains) | 	g.POST("/access-log/:count", a.getAccessLog) | ||||||
| 	g.GET("/logs-blocked/:count", a.getLogsBlockedDomains) | 	g.POST("/error-log/:count", a.getErrorLog) | ||||||
| 	g.POST("/getConfigJson", a.getConfigJson) | 	g.POST("/getConfigJson", a.getConfigJson) | ||||||
| 	g.GET("/getDb", a.getDb) | 	g.GET("/getDb", a.getDb) | ||||||
| 	g.POST("/importDB", a.importDB) | 	g.POST("/importDB", a.importDB) | ||||||
|  | @ -127,15 +127,17 @@ func (a *ServerController) getLogs(c *gin.Context) { | ||||||
| 	jsonObj(c, logs, nil) | 	jsonObj(c, logs, nil) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *ServerController) getLogsSniffedDomains(c *gin.Context) { | func (a *ServerController) getAccessLog(c *gin.Context) { | ||||||
| 	count := c.Param("count") | 	count := c.Param("count") | ||||||
| 	logs := a.serverService.GetLogsSniffedDomains(count) | 	grep := c.PostForm("grep") | ||||||
|  | 	logs := a.serverService.GetAccessLog(count, grep) | ||||||
| 	jsonObj(c, logs, nil) | 	jsonObj(c, logs, nil) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *ServerController) getLogsBlockedDomains(c *gin.Context) { | func (a *ServerController) getErrorLog(c *gin.Context) { | ||||||
| 	count := c.Param("count") | 	count := c.Param("count") | ||||||
| 	logs := a.serverService.GetLogsBlockedDomains(count) | 	grep := c.PostForm("grep") | ||||||
|  | 	logs := a.serverService.GetErrorLog(count, grep) | ||||||
| 	jsonObj(c, logs, nil) | 	jsonObj(c, logs, nil) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ package controller | ||||||
| import ( | import ( | ||||||
| 	"net" | 	"net" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"os" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"x-ui/config" | 	"x-ui/config" | ||||||
|  | @ -13,7 +14,19 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func getRemoteIp(c *gin.Context) string { | func getRemoteIp(c *gin.Context) string { | ||||||
| 	value := c.GetHeader("X-Real-IP") | 	var value string | ||||||
|  | 	priorityHeader := os.Getenv("XUI_GETREMOTEIP_PRIORITY_HEADER") | ||||||
|  | 	if priorityHeader != "" { | ||||||
|  | 		value = c.GetHeader(priorityHeader) | ||||||
|  | 		if strings.Contains(value, ",") { | ||||||
|  | 			ips := strings.Split(value, ",") | ||||||
|  | 			value = ips[0] | ||||||
|  | 		} | ||||||
|  | 		if value != "" { | ||||||
|  | 			return value | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	value = c.GetHeader("X-Real-IP") | ||||||
| 	if value != "" { | 	if value != "" { | ||||||
| 		return value | 		return value | ||||||
| 	} | 	} | ||||||
|  | @ -46,8 +59,13 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) { | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		m.Success = false | 		m.Success = false | ||||||
|  | 		if msg != "" { | ||||||
| 			m.Msg = msg + " " + I18nWeb(c, "fail") + ": " + err.Error() | 			m.Msg = msg + " " + I18nWeb(c, "fail") + ": " + err.Error() | ||||||
| 			logger.Warning(msg+" "+I18nWeb(c, "fail")+": ", err) | 			logger.Warning(msg+" "+I18nWeb(c, "fail")+": ", err) | ||||||
|  | 		} else { | ||||||
|  | 			m.Msg = I18nWeb(c, "fail") + ": " + err.Error() | ||||||
|  | 			logger.Warning(I18nWeb(c, "fail")+": ", err) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	c.JSON(http.StatusOK, m) | 	c.JSON(http.StatusOK, m) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,11 +5,13 @@ import ( | ||||||
| 	_ "unsafe" | 	_ "unsafe" | ||||||
| 
 | 
 | ||||||
| 	"github.com/robfig/cron/v3" | 	"github.com/robfig/cron/v3" | ||||||
|  | 	"github.com/patrickmn/go-cache" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	webServer WebServer | 	webServer WebServer | ||||||
| 	subServer SubServer | 	subServer SubServer | ||||||
|  | 	caching Cache | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type WebServer interface { | type WebServer interface { | ||||||
|  | @ -21,6 +23,11 @@ type SubServer interface { | ||||||
| 	GetCtx() context.Context | 	GetCtx() context.Context | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | type Cache interface { | ||||||
|  | 	Memory() *cache.Cache | ||||||
|  | 	GetCtx() context.Context | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func SetWebServer(s WebServer) { | func SetWebServer(s WebServer) { | ||||||
| 	webServer = s | 	webServer = s | ||||||
| } | } | ||||||
|  | @ -36,3 +43,11 @@ func SetSubServer(s SubServer) { | ||||||
| func GetSubServer() SubServer { | func GetSubServer() SubServer { | ||||||
| 	return subServer | 	return subServer | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func SetCache(c Cache) { | ||||||
|  | 	caching = c | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func GetCache() Cache { | ||||||
|  | 	return caching | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -125,7 +125,8 @@ | ||||||
|                 <a-card hoverable> |                 <a-card hoverable> | ||||||
|                   <b>{{ i18n "menu.link" }}:</b> |                   <b>{{ i18n "menu.link" }}:</b> | ||||||
|                   <a-tag color="purple" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag> |                   <a-tag color="purple" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag> | ||||||
|                   <a-tag color="purple" style="cursor: pointer;" @click="openLogDomains()">{{ i18n "pages.index.logDomains" }}</a-tag> |                   <a-tag color="purple" style="cursor: pointer;" @click="openAccessLog">{{ i18n "pages.index.accessLog" }}</a-tag> | ||||||
|  |                   <a-tag color="purple" style="cursor: pointer;" @click="openErrorLog">{{ i18n "pages.index.errorLog" }}</a-tag> | ||||||
|                   <a-tag color="purple" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag> |                   <a-tag color="purple" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag> | ||||||
|                   <a-tag color="purple" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag> |                   <a-tag color="purple" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag> | ||||||
|                 </a-card> |                 </a-card> | ||||||
|  | @ -315,42 +316,87 @@ | ||||||
|       </a-form> |       </a-form> | ||||||
|       <div class="ant-input" style="height: auto; max-height: 500px; overflow: auto; margin-top: 0.5rem;" v-html="logModal.formattedLogs"></div> |       <div class="ant-input" style="height: auto; max-height: 500px; overflow: auto; margin-top: 0.5rem;" v-html="logModal.formattedLogs"></div> | ||||||
|     </a-modal> |     </a-modal> | ||||||
|     <a-modal id="log-domains-modal" v-model="logDomainsModal.visible" |     <a-modal id="access-log-modal" v-model="accessLogModal.visible" | ||||||
|         :closable="true" @cancel="() => logDomainsModal.visible = false" |         :closable="true" @cancel="() => accessLogModal.visible = false" | ||||||
|         :class="themeSwitcher.currentTheme" |         :class="themeSwitcher.currentTheme" | ||||||
|         width="800px" footer=""> |         width="800px" footer=""> | ||||||
|       <template slot="title"> |       <template slot="title"> | ||||||
|         {{ i18n "pages.index.logDomains" }} |         {{ i18n "pages.index.accessLog" }} | ||||||
|         <a-icon :spin="logDomainsModal.loading" |         <a-icon :spin="accessLogModal.loading" | ||||||
|           type="sync" |           type="sync" | ||||||
|           style="vertical-align: middle; margin-left: 10px;" |           style="vertical-align: middle; margin-left: 10px;" | ||||||
|           :disabled="logDomainsModal.loading" |           :disabled="accessLogModal.loading" | ||||||
|           @click="openLogDomains()"> |           @click="openAccessLog()"> | ||||||
|         </a-icon> |         </a-icon> | ||||||
|       </template> |       </template> | ||||||
|       <a-form layout="inline"> |       <a-form layout="inline"> | ||||||
|         <a-form-item style="margin-right: 0.5rem;"> |         <a-form-item style="margin-right: 0.5rem;"> | ||||||
|           <a-input-group compact> |           <a-input-group compact> | ||||||
|             <a-select size="small" v-model="logDomainsModal.rows" style="width:90px;" |             <a-select size="small" v-model="accessLogModal.rows" style="width:90px;" | ||||||
|                 @change="openLogDomains()" :dropdown-class-name="themeSwitcher.currentTheme"> |                 @change="openAccessLog()" :dropdown-class-name="themeSwitcher.currentTheme"> | ||||||
|               <a-select-option value="500">Few</a-select-option> |               <a-select-option value="500">500</a-select-option> | ||||||
|               <a-select-option value="2500">Medium</a-select-option> |               <a-select-option value="2500">2500</a-select-option> | ||||||
|               <a-select-option value="7000">Many</a-select-option> |               <a-select-option value="7000">7000</a-select-option> | ||||||
|             </a-select> |  | ||||||
|             <a-select size="small" v-model="logDomainsModal.type" style="width:95px;" |  | ||||||
|                 @change="openLogDomains()" :dropdown-class-name="themeSwitcher.currentTheme"> |  | ||||||
|               <a-select-option value="sniffed">Sniffed</a-select-option> |  | ||||||
|               <a-select-option value="blocked">Blocked</a-select-option> |  | ||||||
|             </a-select> |             </a-select> | ||||||
|           </a-input-group> |           </a-input-group> | ||||||
|         </a-form-item> |         </a-form-item> | ||||||
|  |         <a-form-item> | ||||||
|  |           <a-input-search | ||||||
|  |             v-model="accessLogModal.grep" | ||||||
|  |             placeholder="Search.." | ||||||
|  |             size="small" | ||||||
|  |             enter-button | ||||||
|  |             @search="openAccessLog" | ||||||
|  |           /> | ||||||
|  |         </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" | ||||||
|             :href="'data:application/text;charset=utf-8,' + encodeURIComponent(logDomainsModal.logs?.join('\n'))" download="x-ui-domains.log"> |             :href="'data:application/text;charset=utf-8,' + encodeURIComponent(accessLogModal.logs?.join('\n'))" download="xray-access.log"> | ||||||
|           </a-button> |           </a-button> | ||||||
|         </a-form-item> |         </a-form-item> | ||||||
|       </a-form> |       </a-form> | ||||||
|       <div class="ant-input" style="height: auto; max-height: 500px; overflow: auto; margin-top: 0.5rem;" v-html="logDomainsModal.formattedLogs"></div> |       <div class="ant-input" style="height: auto; max-height: 500px; overflow: auto; margin-top: 0.5rem;" v-html="accessLogModal.formattedLogs"></div> | ||||||
|  |     </a-modal> | ||||||
|  |     <a-modal id="error-log-modal" v-model="errorLogModal.visible" | ||||||
|  |         :closable="true" @cancel="() => errorLogModal.visible = false" | ||||||
|  |         :class="themeSwitcher.currentTheme" | ||||||
|  |         width="800px" footer=""> | ||||||
|  |       <template slot="title"> | ||||||
|  |         {{ i18n "pages.index.errorLog" }} | ||||||
|  |         <a-icon :spin="errorLogModal.loading" | ||||||
|  |           type="sync" | ||||||
|  |           style="vertical-align: middle; margin-left: 10px;" | ||||||
|  |           :disabled="errorLogModal.loading" | ||||||
|  |           @click="openErrorLog()"> | ||||||
|  |         </a-icon> | ||||||
|  |       </template> | ||||||
|  |       <a-form layout="inline"> | ||||||
|  |         <a-form-item style="margin-right: 0.5rem;"> | ||||||
|  |           <a-input-group compact> | ||||||
|  |             <a-select size="small" v-model="errorLogModal.rows" style="width:90px;" | ||||||
|  |                 @change="openErrorLog()" :dropdown-class-name="themeSwitcher.currentTheme"> | ||||||
|  |               <a-select-option value="500">500</a-select-option> | ||||||
|  |               <a-select-option value="2500">2500</a-select-option> | ||||||
|  |               <a-select-option value="7000">7000</a-select-option> | ||||||
|  |             </a-select> | ||||||
|  |           </a-input-group> | ||||||
|  |         </a-form-item> | ||||||
|  |         <a-form-item> | ||||||
|  |           <a-input-search | ||||||
|  |             v-model="errorLogModal.grep" | ||||||
|  |             placeholder="Search.." | ||||||
|  |             size="small" | ||||||
|  |             enter-button | ||||||
|  |             @search="openErrorLog" | ||||||
|  |           /> | ||||||
|  |         </a-form-item> | ||||||
|  |         <a-form-item style="float: right;"> | ||||||
|  |           <a-button type="primary" icon="download" | ||||||
|  |             :href="'data:application/text;charset=utf-8,' + encodeURIComponent(errorLogModal.logs?.join('\n'))" download="xray-error.log"> | ||||||
|  |           </a-button> | ||||||
|  |         </a-form-item> | ||||||
|  |       </a-form> | ||||||
|  |       <div class="ant-input" style="height: auto; max-height: 500px; overflow: auto; margin-top: 0.5rem;" v-html="errorLogModal.formattedLogs"></div> | ||||||
|     </a-modal> |     </a-modal> | ||||||
|     <a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title" |     <a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title" | ||||||
|         :closable="true" footer="" |         :closable="true" footer="" | ||||||
|  | @ -528,38 +574,99 @@ | ||||||
|         }, |         }, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const logDomainsModal = { |     const accessLogModal = { | ||||||
|         visible: false, |         visible: false, | ||||||
|         logs: [], |         logs: [], | ||||||
|         rows: 500, |         rows: 500, | ||||||
|         type: 'sniffed', |         grep: '', | ||||||
|         loading: false, |         loading: false, | ||||||
|         show(logs) { |         show(logs) { | ||||||
|             this.visible = true; |             this.visible = true; | ||||||
|             this.logs = logs;  |             this.logs = logs;  | ||||||
|             this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs, this.type) : "No Record..."; |             this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record..."; | ||||||
|         }, |         }, | ||||||
|         formatLogs(logs, type) { |         formatLogs(logs) { | ||||||
|             let formattedLogs = ''; |             let formattedLogs = ''; | ||||||
|  |             const levelColors = ["#3c89e8","#008771","#008771","#f37b24","#e04141","#bcbcbc"]; | ||||||
| 
 | 
 | ||||||
|             logs.forEach((log, index) => { |             logs.forEach((log, index) => { | ||||||
|                 let [data, message] = log.split(" - ",2); |               if (log.length <= 3) { | ||||||
|                 const parts = data.split(" "); |                 return; | ||||||
|                 if(index>0) formattedLogs += '<br>'; |               } | ||||||
|  |               let [date, time] = log.split(' ', 2); | ||||||
|  |               let message = log.substr(date?.length !== undefined && time?.length !== undefined ? (date.length+time.length+2) : 0); | ||||||
|  |               let messageColor = levelColors[5]; | ||||||
|  |               if (message && message.indexOf('-> blocked') !== -1) { | ||||||
|  |                 messageColor = levelColors[4]; | ||||||
|  |               } | ||||||
|                |                | ||||||
|                 if (parts.length === 2) { |               if (index > 0) formattedLogs += '<br/>'; | ||||||
|                     const d = parts[0]; |               formattedLogs += `<span style="color: ${levelColors[0]};">${date} ${time}</span> `; | ||||||
|                     const t = parts[1]; |               formattedLogs += `- <span style="color: ${messageColor};">${message}</span>`; | ||||||
|                     formattedLogs += `<span style="color: gray;">${d} ${t}</span>`; |             }); | ||||||
|  | 
 | ||||||
|  |             return formattedLogs; | ||||||
|  |         }, | ||||||
|  |         hide() { | ||||||
|  |             this.visible = false; | ||||||
|  |         }, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const errorLogModal = { | ||||||
|  |         visible: false, | ||||||
|  |         logs: [], | ||||||
|  |         rows: 500, | ||||||
|  |         grep: '', | ||||||
|  |         loading: false, | ||||||
|  |         show(logs) { | ||||||
|  |             this.visible = true; | ||||||
|  |             this.logs = logs; | ||||||
|  |             this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record..."; | ||||||
|  |         }, | ||||||
|  |         formatLogs(logs) { | ||||||
|  |             let formattedLogs = ''; | ||||||
|  |             const levels = ["DEBUG","INFO","NOTICE","WARNING","ERROR"]; | ||||||
|  |             const levelsMap = {"[Debug]": levels[0], "[Info]": levels[1], "[Notice]": levels[2], "[Warning]": levels[3], "[Error]": levels[4]}; | ||||||
|  |             const levelColors = ["#3c89e8","#008771","#008771","#f37b24","#e04141","#bcbcbc"]; | ||||||
|  |             const idColors = ['#CADABF','#5F6F65','#FFDFD6','#BC9F8B','#C9DABF','#9CA986','#808D7C','#E7E8D8','#B5CFB7']; | ||||||
|  |             let idColorIndex = 0; | ||||||
|  |             let lastLogId = ''; | ||||||
|  | 
 | ||||||
|  |             logs.forEach((log, index) => { | ||||||
|  |               if (log.length <= 3) { | ||||||
|  |                 return; | ||||||
|  |               } | ||||||
|  |               let [date, time, levelTag, id] = log.split(' ', 4); | ||||||
|  |               if (date?.length === undefined || time?.length === undefined || levelTag?.length === undefined || id?.length === undefined) { | ||||||
|  |                 if (index > 0) formattedLogs += '<br/>'; | ||||||
|  |                 formattedLogs += log; | ||||||
|               } else { |               } else { | ||||||
|                     formattedLogs += `<span style="color: gray;">${data}</span>`; |                 let message = log.substr( | ||||||
|                 } |                   date?.length !== undefined && time?.length !== undefined && levelTag?.length !== undefined && id?.length !== undefined ? | ||||||
|  |                     (date.length + time.length + levelTag.length + id.length + 4) : 0 | ||||||
|  |                 ); | ||||||
|  |                 let level = levelsMap[levelTag]; | ||||||
|  |                 const levelIndex = levels.indexOf(level, levels) || 5; | ||||||
| 
 | 
 | ||||||
|                 if (message) { |                 if (index > 0) formattedLogs += '<br/>'; | ||||||
|                     message = "<b>"+(type === 'sniffed' ? 'Sniffed' : 'Blocked')+": </b>" + message; |                 formattedLogs += `<span style="color: ${levelColors[0]};">${date} ${time}</span> `; | ||||||
|                 } |                 formattedLogs += `<span style="color: ${levelColors[levelIndex]};">${level}</span> `; | ||||||
|  |                 formattedLogs += ' - '; | ||||||
|                  |                  | ||||||
|                 formattedLogs += message ? ' - ' + message : ''; |                 if (id.substr(0, 1) === '[') { | ||||||
|  |                   if (lastLogId !== '' && lastLogId !== id) { | ||||||
|  |                     idColorIndex++; | ||||||
|  |                   } | ||||||
|  |                   if (idColorIndex >= idColors.length) { | ||||||
|  |                     idColorIndex = 0; | ||||||
|  |                   } | ||||||
|  |                   let idColor = idColors[idColorIndex]; | ||||||
|  |                   lastLogId = id; | ||||||
|  |                   formattedLogs += `<span style="color: ${idColor};">${id}</span> <span style="color: ${levelColors[5]};">${message}</span>`; | ||||||
|  |                 } else { | ||||||
|  |                   formattedLogs += `<span style="color: ${levelColors[5]};">${id} ${message}</span>`; | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|             return formattedLogs; |             return formattedLogs; | ||||||
|  | @ -674,15 +781,25 @@ | ||||||
|                 await PromiseUtil.sleep(500); |                 await PromiseUtil.sleep(500); | ||||||
|                 logModal.loading = false; |                 logModal.loading = false; | ||||||
|             }, |             }, | ||||||
|             async openLogDomains(){ |             async openAccessLog() { | ||||||
|                 logDomainsModal.loading = true; |                 accessLogModal.loading = true; | ||||||
|                 const msg = await HttpUtil.get('server/logs-'+(logDomainsModal.type==='blocked'?'blocked':'sniffed')+'/'+logDomainsModal.rows); |                 const msg = await HttpUtil.post('server/access-log/'+accessLogModal.rows, { grep: accessLogModal.grep }); | ||||||
|                 if (!msg.success) { |                 if (!msg.success) { | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|                 logDomainsModal.show(msg.obj); |                 accessLogModal.show(msg.obj); | ||||||
|                 await PromiseUtil.sleep(500); |                 await PromiseUtil.sleep(500); | ||||||
|                 logDomainsModal.loading = false; |                 accessLogModal.loading = false; | ||||||
|  |             }, | ||||||
|  |             async openErrorLog() { | ||||||
|  |                 errorLogModal.loading = true; | ||||||
|  |                 const msg = await HttpUtil.post('server/error-log/'+errorLogModal.rows, { grep: errorLogModal.grep }); | ||||||
|  |                 if (!msg.success) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 errorLogModal.show(msg.obj); | ||||||
|  |                 await PromiseUtil.sleep(500); | ||||||
|  |                 errorLogModal.loading = false; | ||||||
|             }, |             }, | ||||||
|             async openConfig() { |             async openConfig() { | ||||||
|                 this.loading(true); |                 this.loading(true); | ||||||
|  |  | ||||||
|  | @ -1855,6 +1855,18 @@ func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.Client | ||||||
| 	return traffic, nil | 	return traffic, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (s *InboundService) GetClientOnlineIPs(email string) (int, error) { | ||||||
|  | 	s.xrayApi.Init(p.GetAPIPort()) | ||||||
|  | 	defer s.xrayApi.Close() | ||||||
|  | 
 | ||||||
|  | 	count, err := s.xrayApi.GetClientOnlineIPs(email) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Debug("Failed to fetch Xray Client Online IPs:", err) | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	return count, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) { | func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) { | ||||||
| 	db := database.GetDB() | 	db := database.GetDB() | ||||||
| 	InboundClientIps := &model.InboundClientIps{} | 	InboundClientIps := &model.InboundClientIps{} | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ import ( | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  | 	"regexp" | ||||||
| 
 | 
 | ||||||
| 	"x-ui/config" | 	"x-ui/config" | ||||||
| 	"x-ui/database" | 	"x-ui/database" | ||||||
|  | @ -22,6 +23,7 @@ import ( | ||||||
| 	"x-ui/util/common" | 	"x-ui/util/common" | ||||||
| 	"x-ui/util/sys" | 	"x-ui/util/sys" | ||||||
| 	"x-ui/xray" | 	"x-ui/xray" | ||||||
|  | 	"x-ui/web/global" | ||||||
| 
 | 
 | ||||||
| 	"github.com/shirou/gopsutil/v4/cpu" | 	"github.com/shirou/gopsutil/v4/cpu" | ||||||
| 	"github.com/shirou/gopsutil/v4/disk" | 	"github.com/shirou/gopsutil/v4/disk" | ||||||
|  | @ -62,6 +64,9 @@ type Status struct { | ||||||
| 		ErrorMsg string       `json:"errorMsg"` | 		ErrorMsg string       `json:"errorMsg"` | ||||||
| 		Version  string       `json:"version"` | 		Version  string       `json:"version"` | ||||||
| 	} `json:"xray"` | 	} `json:"xray"` | ||||||
|  | 	XUI struct { | ||||||
|  | 		LatestVersion string `json:"latestVersion"` | ||||||
|  | 	} `json:"xui"` | ||||||
| 	Uptime   uint64    `json:"uptime"` | 	Uptime   uint64    `json:"uptime"` | ||||||
| 	Loads    []float64 `json:"loads"` | 	Loads    []float64 `json:"loads"` | ||||||
| 	TcpCount int       `json:"tcpCount"` | 	TcpCount int       `json:"tcpCount"` | ||||||
|  | @ -94,6 +99,14 @@ type ServerService struct { | ||||||
| 	inboundService InboundService | 	inboundService InboundService | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func extractValue(body string, key string) string { | ||||||
|  |     keystr := "\"" + key + "\":[^,;\\]}]*" | ||||||
|  |     r, _ := regexp.Compile(keystr) | ||||||
|  |     match := r.FindString(body) | ||||||
|  |     keyValMatch := strings.Split(match, ":") | ||||||
|  |     return strings.TrimSpace(strings.ReplaceAll(keyValMatch[1], "\"", "")) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func getPublicIP(url string) string { | func getPublicIP(url string) string { | ||||||
| 	var host string | 	var host string | ||||||
| 	host = os.Getenv("XUI_SERVER_IP") | 	host = os.Getenv("XUI_SERVER_IP") | ||||||
|  | @ -120,7 +133,37 @@ func getPublicIP(url string) string { | ||||||
| 	return ipString | 	return ipString | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func getXuiLatestVersion() string { | ||||||
|  | 	cache := global.GetCache().Memory() | ||||||
|  | 	if data, found := cache.Get("xui_latest_tag_name"); found { | ||||||
|  | 		if tag, ok := data.(string); ok { | ||||||
|  | 			return string(tag) | ||||||
|  | 		} else { | ||||||
|  | 			return "" | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		url := "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | ||||||
|  | 		 | ||||||
|  | 		resp, err := http.Get(url) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "" | ||||||
|  | 		} | ||||||
|  | 		defer resp.Body.Close() | ||||||
|  | 		 | ||||||
|  | 		json, err := io.ReadAll(resp.Body) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "" | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		tag := extractValue(string(json), "tag_name") | ||||||
|  | 		cache.Set("xui_latest_tag_name", tag, 60*time.Minute) | ||||||
|  | 		return tag | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (s *ServerService) GetStatus(lastStatus *Status) *Status { | func (s *ServerService) GetStatus(lastStatus *Status) *Status { | ||||||
|  | 	cache := global.GetCache().Memory() | ||||||
|  | 
 | ||||||
| 	now := time.Now() | 	now := time.Now() | ||||||
| 	status := &Status{ | 	status := &Status{ | ||||||
| 		T: now, | 		T: now, | ||||||
|  | @ -223,8 +266,27 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status { | ||||||
| 		logger.Warning("get udp connections failed:", err) | 		logger.Warning("get udp connections failed:", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if data, found := cache.Get("xui_public_ipv4"); found { | ||||||
|  | 		if ipv4, ok := data.(string); ok { | ||||||
|  | 			status.PublicIP.IPv4 = string(ipv4) | ||||||
|  | 		} else { | ||||||
|  | 			status.PublicIP.IPv4 = "N/A" | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
| 		status.PublicIP.IPv4 = getPublicIP("https://api.ipify.org") | 		status.PublicIP.IPv4 = getPublicIP("https://api.ipify.org") | ||||||
|  | 		cache.Set("xui_public_ipv4", status.PublicIP.IPv4, 720*time.Hour) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if data, found := cache.Get("xui_public_ipv6"); found { | ||||||
|  | 		if ipv6, ok := data.(string); ok { | ||||||
|  | 			status.PublicIP.IPv6 = string(ipv6) | ||||||
|  | 		} else { | ||||||
|  | 			status.PublicIP.IPv6 = "N/A" | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
| 		status.PublicIP.IPv6 = getPublicIP("https://api6.ipify.org") | 		status.PublicIP.IPv6 = getPublicIP("https://api6.ipify.org") | ||||||
|  | 		cache.Set("xui_public_ipv6", status.PublicIP.IPv6, 720*time.Hour) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	if s.xrayService.IsXrayRunning() { | 	if s.xrayService.IsXrayRunning() { | ||||||
| 		status.Xray.State = Running | 		status.Xray.State = Running | ||||||
|  | @ -239,6 +301,9 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status { | ||||||
| 		status.Xray.ErrorMsg = s.xrayService.GetXrayResult() | 		status.Xray.ErrorMsg = s.xrayService.GetXrayResult() | ||||||
| 	} | 	} | ||||||
| 	status.Xray.Version = s.xrayService.GetXrayVersion() | 	status.Xray.Version = s.xrayService.GetXrayVersion() | ||||||
|  | 
 | ||||||
|  | 	status.XUI.LatestVersion = getXuiLatestVersion() | ||||||
|  | 
 | ||||||
| 	var rtm runtime.MemStats | 	var rtm runtime.MemStats | ||||||
| 	runtime.ReadMemStats(&rtm) | 	runtime.ReadMemStats(&rtm) | ||||||
| 
 | 
 | ||||||
|  | @ -448,22 +513,56 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str | ||||||
| 	return lines | 	return lines | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *ServerService) GetLogsSniffedDomains(count string) []string { | func (s *ServerService) GetAccessLog(count string, grep string) []string { | ||||||
| 	c, _ := strconv.Atoi(count) | 	accessLogPath, err := xray.GetAccessLogPath() | ||||||
| 	var lines []string | 	if err != nil { | ||||||
|  | 		return []string{"Error in Access Log retrieval: " + err.Error()} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	lines = logger.GetLogsSniffedDomains(c) | 	if accessLogPath != "none" && accessLogPath != "" { | ||||||
| 
 | 		var cmdArgs []string | ||||||
| 	return lines | 		if grep != "" { | ||||||
|  | 			cmdArgs = []string{"bash", "-c", fmt.Sprintf("tail -n %s %s | grep '%s' | sort -r", count, accessLogPath, grep)} | ||||||
|  |     	} else { | ||||||
|  |     		cmdArgs = []string{"bash", "-c", fmt.Sprintf("tail -n %s %s | sort -r", count, accessLogPath)} | ||||||
|  |     	} | ||||||
|  | 		cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) | ||||||
|  | 		var out bytes.Buffer | ||||||
|  | 		cmd.Stdout = &out | ||||||
|  | 		err := cmd.Run() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return []string{"Failed to run command: " + err.Error()} | ||||||
|  | 		} | ||||||
|  | 		return strings.Split(out.String(), "\n") | ||||||
|  | 	} else { | ||||||
|  | 		return []string{"Access Log disabled!"} | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *ServerService) GetLogsBlockedDomains(count string) []string { | func (s *ServerService) GetErrorLog(count string, grep string) []string { | ||||||
| 	c, _ := strconv.Atoi(count) | 	errorLogPath, err := xray.GetErrorLogPath() | ||||||
| 	var lines []string | 	if err != nil { | ||||||
|  | 		return []string{"Error in Error Log retrieval: " + err.Error()} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	lines = logger.GetLogsBlockedDomains(c) | 	if errorLogPath != "none" && errorLogPath != "" { | ||||||
| 
 | 		var cmdArgs []string | ||||||
| 	return lines | 		if grep != "" { | ||||||
|  | 			cmdArgs = []string{"bash", "-c", fmt.Sprintf("tail -n %s %s | grep '%s' | sort -r", count, errorLogPath, grep)} | ||||||
|  |     	} else { | ||||||
|  |     		cmdArgs = []string{"bash", "-c", fmt.Sprintf("tail -n %s %s | sort -r", count, errorLogPath)} | ||||||
|  |     	} | ||||||
|  | 		cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) | ||||||
|  | 		var out bytes.Buffer | ||||||
|  | 		cmd.Stdout = &out | ||||||
|  | 		err := cmd.Run() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return []string{"Failed to run command: " + err.Error()} | ||||||
|  | 		} | ||||||
|  | 		return strings.Split(out.String(), "\n") | ||||||
|  | 	} else { | ||||||
|  | 		return []string{"Error Log disabled!"} | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *ServerService) GetConfigJson() (interface{}, error) { | func (s *ServerService) GetConfigJson() (interface{}, error) { | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"sync" | 	"sync" | ||||||
|  | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"x-ui/logger" | 	"x-ui/logger" | ||||||
| 	"x-ui/xray" | 	"x-ui/xray" | ||||||
|  | @ -56,6 +57,13 @@ func (s *XrayService) GetXrayVersion() string { | ||||||
| 	return p.GetVersion() | 	return p.GetVersion() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (s *XrayService) GetXrayApiPort() string { | ||||||
|  | 	if p == nil { | ||||||
|  | 		return "Unknown" | ||||||
|  | 	} | ||||||
|  | 	return strconv.Itoa(p.GetAPIPort()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func RemoveIndex(s []interface{}, index int) []interface{} { | func RemoveIndex(s []interface{}, index int) []interface{} { | ||||||
| 	return append(s[:index], s[index+1:]...) | 	return append(s[:index], s[index+1:]...) | ||||||
| } | } | ||||||
|  | @ -205,6 +213,10 @@ func (s *XrayService) RestartXray(isForce bool) error { | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | 	if isForce { | ||||||
|  | 		logger.Debug("Xray Api Port: ", strconv.Itoa(p.GetAPIPort())) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -105,7 +105,8 @@ | ||||||
| "xraySwitchVersionDialogDesc" = "Are you sure you want to change the Xray version to" | "xraySwitchVersionDialogDesc" = "Are you sure you want to change the Xray version to" | ||||||
| "dontRefresh" = "Installation is in progress, please do not refresh this page" | "dontRefresh" = "Installation is in progress, please do not refresh this page" | ||||||
| "logs" = "Logs" | "logs" = "Logs" | ||||||
| "logDomains" = "Log Domains" | "accessLog" = "Access Log" | ||||||
|  | "errorLog" = "Error Log" | ||||||
| "config" = "Config" | "config" = "Config" | ||||||
| "backup" = "Backup & Restore" | "backup" = "Backup & Restore" | ||||||
| "backupTitle" = "Database Backup & Restore" | "backupTitle" = "Database Backup & Restore" | ||||||
|  |  | ||||||
|  | @ -105,7 +105,8 @@ | ||||||
| "xraySwitchVersionDialogDesc" = "¿Estás seguro de que deseas cambiar la versión de Xray a" | "xraySwitchVersionDialogDesc" = "¿Estás seguro de que deseas cambiar la versión de Xray a" | ||||||
| "dontRefresh" = "La instalación está en progreso, por favor no actualices esta página." | "dontRefresh" = "La instalación está en progreso, por favor no actualices esta página." | ||||||
| "logs" = "Registros" | "logs" = "Registros" | ||||||
| "logDomains" = "Log Domains" | "accessLog" = "Access Log" | ||||||
|  | "errorLog" = "Error Log" | ||||||
| "config" = "Configuración" | "config" = "Configuración" | ||||||
| "backup" = "Copia de Seguridad y Restauración" | "backup" = "Copia de Seguridad y Restauración" | ||||||
| "backupTitle" = "Copia de Seguridad y Restauración de la Base de Datos" | "backupTitle" = "Copia de Seguridad y Restauración de la Base de Datos" | ||||||
|  |  | ||||||
|  | @ -105,7 +105,8 @@ | ||||||
| "xraySwitchVersionDialogDesc" = "آیا از تغییر نسخه مطمئن هستید؟" | "xraySwitchVersionDialogDesc" = "آیا از تغییر نسخه مطمئن هستید؟" | ||||||
| "dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید" | "dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید" | ||||||
| "logs" = "گزارشها" | "logs" = "گزارشها" | ||||||
| "logDomains" = "Log Domains" | "accessLog" = "Access Log" | ||||||
|  | "errorLog" = "Error Log" | ||||||
| "config" = "پیکربندی" | "config" = "پیکربندی" | ||||||
| "backup" = "پشتیبانگیری" | "backup" = "پشتیبانگیری" | ||||||
| "backupTitle" = "پشتیبانگیری دیتابیس" | "backupTitle" = "پشتیبانگیری دیتابیس" | ||||||
|  |  | ||||||
|  | @ -105,7 +105,8 @@ | ||||||
| "xraySwitchVersionDialogDesc" = "Apakah Anda yakin ingin mengubah versi Xray menjadi" | "xraySwitchVersionDialogDesc" = "Apakah Anda yakin ingin mengubah versi Xray menjadi" | ||||||
| "dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini" | "dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini" | ||||||
| "logs" = "Log" | "logs" = "Log" | ||||||
| "logDomains" = "Log Domains" | "accessLog" = "Access Log" | ||||||
|  | "errorLog" = "Error Log" | ||||||
| "config" = "Konfigurasi" | "config" = "Konfigurasi" | ||||||
| "backup" = "Cadangan & Pulihkan" | "backup" = "Cadangan & Pulihkan" | ||||||
| "backupTitle" = "Cadangan & Pulihkan Database" | "backupTitle" = "Cadangan & Pulihkan Database" | ||||||
|  |  | ||||||
|  | @ -105,7 +105,8 @@ | ||||||
| "xraySwitchVersionDialogDesc" = "Xrayのバージョンを切り替えますか?" | "xraySwitchVersionDialogDesc" = "Xrayのバージョンを切り替えますか?" | ||||||
| "dontRefresh" = "インストール中、このページをリロードしないでください" | "dontRefresh" = "インストール中、このページをリロードしないでください" | ||||||
| "logs" = "ログ" | "logs" = "ログ" | ||||||
| "logDomains" = "Log Domains" | "accessLog" = "Access Log" | ||||||
|  | "errorLog" = "Error Log" | ||||||
| "config" = "設定" | "config" = "設定" | ||||||
| "backup" = "バックアップと復元" | "backup" = "バックアップと復元" | ||||||
| "backupTitle" = "データベースのバックアップと復元" | "backupTitle" = "データベースのバックアップと復元" | ||||||
|  |  | ||||||
|  | @ -105,7 +105,8 @@ | ||||||
| "xraySwitchVersionDialogDesc" = "Tem certeza de que deseja alterar a versão do Xray para" | "xraySwitchVersionDialogDesc" = "Tem certeza de que deseja alterar a versão do Xray para" | ||||||
| "dontRefresh" = "Instalação em andamento, por favor não atualize a página" | "dontRefresh" = "Instalação em andamento, por favor não atualize a página" | ||||||
| "logs" = "Logs" | "logs" = "Logs" | ||||||
| "logDomains" = "Log Domains" | "accessLog" = "Access Log" | ||||||
|  | "errorLog" = "Error Log" | ||||||
| "config" = "Configuração" | "config" = "Configuração" | ||||||
| "backup" = "Backup e Restauração" | "backup" = "Backup e Restauração" | ||||||
| "backupTitle" = "Backup e Restauração do Banco de Dados" | "backupTitle" = "Backup e Restauração do Banco de Dados" | ||||||
|  |  | ||||||
|  | @ -105,7 +105,8 @@ | ||||||
| "xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?" | "xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?" | ||||||
| "dontRefresh" = "Идёт установка. Пожалуйста, не обновляйте эту страницу" | "dontRefresh" = "Идёт установка. Пожалуйста, не обновляйте эту страницу" | ||||||
| "logs" = "Логи" | "logs" = "Логи" | ||||||
| "logDomains" = "Логи доменов" | "accessLog" = "Access Лог" | ||||||
|  | "errorLog" = "Error Лог" | ||||||
| "config" = "Конфигурация" | "config" = "Конфигурация" | ||||||
| "backup" = "Бэкап и восстановление" | "backup" = "Бэкап и восстановление" | ||||||
| "backupTitle" = "База данных бэкапа и восстановления" | "backupTitle" = "База данных бэкапа и восстановления" | ||||||
|  |  | ||||||
|  | @ -105,7 +105,8 @@ | ||||||
| "xraySwitchVersionDialogDesc" = "Xray sürümünü değiştirmek istediğinizden emin misiniz" | "xraySwitchVersionDialogDesc" = "Xray sürümünü değiştirmek istediğinizden emin misiniz" | ||||||
| "dontRefresh" = "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin" | "dontRefresh" = "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin" | ||||||
| "logs" = "Günlükler" | "logs" = "Günlükler" | ||||||
| "logDomains" = "Log Domains" | "accessLog" = "Access Log" | ||||||
|  | "errorLog" = "Error Log" | ||||||
| "config" = "Yapılandırma" | "config" = "Yapılandırma" | ||||||
| "backup" = "Yedekle & Geri Yükle" | "backup" = "Yedekle & Geri Yükle" | ||||||
| "backupTitle" = "Veritabanı Yedekleme & Geri Yükleme" | "backupTitle" = "Veritabanı Yedekleme & Geri Yükleme" | ||||||
|  |  | ||||||
|  | @ -105,7 +105,8 @@ | ||||||
| "xraySwitchVersionDialogDesc" = "Ви впевнені, що бажаєте змінити версію Xray на" | "xraySwitchVersionDialogDesc" = "Ви впевнені, що бажаєте змінити версію Xray на" | ||||||
| "dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку" | "dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку" | ||||||
| "logs" = "Журнали" | "logs" = "Журнали" | ||||||
| "logDomains" = "Логи доменов" | "accessLog" = "Access Лог" | ||||||
|  | "errorLog" = "Error Лог" | ||||||
| "config" = "Конфігурація" | "config" = "Конфігурація" | ||||||
| "backup" = "Резервне копіювання та відновлення" | "backup" = "Резервне копіювання та відновлення" | ||||||
| "backupTitle" = "Резервне копіювання та відновлення бази даних" | "backupTitle" = "Резервне копіювання та відновлення бази даних" | ||||||
|  |  | ||||||
|  | @ -105,7 +105,8 @@ | ||||||
| "xraySwitchVersionDialogDesc" = "Bạn có chắc chắn muốn chuyển đổi phiên bản Xray sang" | "xraySwitchVersionDialogDesc" = "Bạn có chắc chắn muốn chuyển đổi phiên bản Xray sang" | ||||||
| "dontRefresh" = "Đang tiến hành cài đặt, vui lòng không làm mới trang này." | "dontRefresh" = "Đang tiến hành cài đặt, vui lòng không làm mới trang này." | ||||||
| "logs" = "Nhật ký" | "logs" = "Nhật ký" | ||||||
| "logDomains" = "Log Domains" | "accessLog" = "Access Log" | ||||||
|  | "errorLog" = "Error Log" | ||||||
| "config" = "Cấu hình" | "config" = "Cấu hình" | ||||||
| "backup" = "Sao lưu & Khôi phục" | "backup" = "Sao lưu & Khôi phục" | ||||||
| "backupTitle" = "Sao lưu & Khôi phục Cơ sở dữ liệu" | "backupTitle" = "Sao lưu & Khôi phục Cơ sở dữ liệu" | ||||||
|  |  | ||||||
|  | @ -105,7 +105,8 @@ | ||||||
| "xraySwitchVersionDialogDesc" = "是否切换 Xray 版本至" | "xraySwitchVersionDialogDesc" = "是否切换 Xray 版本至" | ||||||
| "dontRefresh" = "安装中,请勿刷新此页面" | "dontRefresh" = "安装中,请勿刷新此页面" | ||||||
| "logs" = "日志" | "logs" = "日志" | ||||||
| "logDomains" = "Log Domains" | "accessLog" = "Access Log" | ||||||
|  | "errorLog" = "Error Log" | ||||||
| "config" = "配置" | "config" = "配置" | ||||||
| "backup" = "备份和恢复" | "backup" = "备份和恢复" | ||||||
| "backupTitle" = "备份和恢复数据库" | "backupTitle" = "备份和恢复数据库" | ||||||
|  |  | ||||||
|  | @ -105,7 +105,8 @@ | ||||||
| "xraySwitchVersionDialogDesc" = "是否切換 Xray 版本至" | "xraySwitchVersionDialogDesc" = "是否切換 Xray 版本至" | ||||||
| "dontRefresh" = "安裝中,請勿重新整理此頁面" | "dontRefresh" = "安裝中,請勿重新整理此頁面" | ||||||
| "logs" = "日誌" | "logs" = "日誌" | ||||||
| "logDomains" = "Log Domains" | "accessLog" = "Access Log" | ||||||
|  | "errorLog" = "Error Log" | ||||||
| "config" = "配置" | "config" = "配置" | ||||||
| "backup" = "備份和恢復" | "backup" = "備份和恢復" | ||||||
| "backupTitle" = "備份和恢復資料庫" | "backupTitle" = "備份和恢復資料庫" | ||||||
|  |  | ||||||
							
								
								
									
										29
									
								
								xray/api.go
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								xray/api.go
									
									
									
									
									
								
							|  | @ -204,6 +204,35 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) { | ||||||
| 	return mapToSlice(tagTrafficMap), mapToSlice(emailTrafficMap), nil | 	return mapToSlice(tagTrafficMap), mapToSlice(emailTrafficMap), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (x *XrayAPI) GetClientOnlineIPs(email string) (int, error) { | ||||||
|  | 	if x.grpcClient == nil { | ||||||
|  | 		return 0, common.NewError("xray api is not initialized") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	statName := "user>>>" + email + ">>>online" | ||||||
|  | 
 | ||||||
|  | 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) | ||||||
|  | 	defer cancel() | ||||||
|  | 
 | ||||||
|  | 	if x.StatsServiceClient == nil { | ||||||
|  | 		return 0, common.NewError("xray StatusServiceClient is not initialized") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r := &statsService.GetStatsRequest{ | ||||||
|  | 		Name:   statName, | ||||||
|  | 		Reset_: false, | ||||||
|  | 	} | ||||||
|  | 	resp, err := (*x.StatsServiceClient).GetStatsOnline(ctx, r) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Debug("Failed to query Xray statsonline:", err) | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	count := resp.GetStat().Value | ||||||
|  | 	 | ||||||
|  | 	return int(count), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func processTraffic(matches []string, value int64, trafficMap map[string]*Traffic) { | func processTraffic(matches []string, value int64, trafficMap map[string]*Traffic) { | ||||||
| 	isInbound := matches[1] == "inbound" | 	isInbound := matches[1] == "inbound" | ||||||
| 	tag := matches[2] | 	tag := matches[2] | ||||||
|  |  | ||||||
|  | @ -81,6 +81,30 @@ func GetAccessLogPath() (string, error) { | ||||||
| 	return "", err | 	return "", err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func GetErrorLogPath() (string, error) { | ||||||
|  | 	config, err := os.ReadFile(GetConfigPath()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Warningf("Failed to read configuration file: %s", err) | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	jsonConfig := map[string]interface{}{} | ||||||
|  | 	err = json.Unmarshal([]byte(config), &jsonConfig) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Warningf("Failed to parse JSON configuration: %s", err) | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if jsonConfig["log"] != nil { | ||||||
|  | 		jsonLog := jsonConfig["log"].(map[string]interface{}) | ||||||
|  | 		if jsonLog["error"] != nil { | ||||||
|  | 			errorLogPath := jsonLog["error"].(string) | ||||||
|  | 			return errorLogPath, nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return "", err | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func stopProcess(p *Process) { | func stopProcess(p *Process) { | ||||||
| 	p.Stop() | 	p.Stop() | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue
	
	 serogaq
						serogaq