mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-05-05 14:48:49 +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
|
@ -9,4 +9,5 @@ XUI_VLESS_SNI=""
|
||||||
#XUI_SUB_SUPPORT_URL=""
|
#XUI_SUB_SUPPORT_URL=""
|
||||||
#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
|
||||||
m.Msg = msg + " " + I18nWeb(c, "fail") + ": " + err.Error()
|
if msg != "" {
|
||||||
logger.Warning(msg+" "+I18nWeb(c, "fail")+": ", err)
|
m.Msg = msg + " " + I18nWeb(c, "fail") + ": " + err.Error()
|
||||||
|
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 (index > 0) formattedLogs += '<br/>';
|
||||||
|
formattedLogs += `<span style="color: ${levelColors[0]};">${date} ${time}</span> `;
|
||||||
|
formattedLogs += `- <span style="color: ${messageColor};">${message}</span>`;
|
||||||
|
});
|
||||||
|
|
||||||
if (parts.length === 2) {
|
return formattedLogs;
|
||||||
const d = parts[0];
|
},
|
||||||
const t = parts[1];
|
hide() {
|
||||||
formattedLogs += `<span style="color: gray;">${d} ${t}</span>`;
|
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 {
|
||||||
|
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 (index > 0) formattedLogs += '<br/>';
|
||||||
|
formattedLogs += `<span style="color: ${levelColors[0]};">${date} ${time}</span> `;
|
||||||
|
formattedLogs += `<span style="color: ${levelColors[levelIndex]};">${level}</span> `;
|
||||||
|
formattedLogs += ' - ';
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
formattedLogs += `<span style="color: gray;">${data}</span>`;
|
formattedLogs += `<span style="color: ${levelColors[5]};">${id} ${message}</span>`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (message) {
|
|
||||||
message = "<b>"+(type === 'sniffed' ? 'Sniffed' : 'Blocked')+": </b>" + message;
|
|
||||||
}
|
|
||||||
|
|
||||||
formattedLogs += message ? ' - ' + message : '';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
status.PublicIP.IPv4 = getPublicIP("https://api.ipify.org")
|
if data, found := cache.Get("xui_public_ipv4"); found {
|
||||||
status.PublicIP.IPv6 = getPublicIP("https://api6.ipify.org")
|
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")
|
||||||
|
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")
|
||||||
|
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