From 42d5dcb17ecaf8826f2ff5a61b621e9bd823913a Mon Sep 17 00:00:00 2001
From: serogaq <36307024+serogaq@users.noreply.github.com>
Date: Tue, 10 Dec 2024 16:31:43 +0300
Subject: [PATCH 1/5] feature/9
9 / ClientOnlineIPs api
---
web/controller/inbound.go | 20 ++++++++++++++++++++
web/controller/util.go | 9 +++++++--
web/service/inbound.go | 12 ++++++++++++
web/service/server.go | 2 ++
web/service/xray.go | 8 ++++++++
xray/api.go | 29 +++++++++++++++++++++++++++++
6 files changed, 78 insertions(+), 2 deletions(-)
diff --git a/web/controller/inbound.go b/web/controller/inbound.go
index c22ce192..d572cc40 100644
--- a/web/controller/inbound.go
+++ b/web/controller/inbound.go
@@ -12,6 +12,10 @@ import (
"github.com/gin-gonic/gin"
)
+type ClientOnlineIPsResponse struct {
+ Count int `json:"count"`
+}
+
type InboundController struct {
inboundService service.InboundService
xrayService service.XrayService
@@ -30,6 +34,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound)
+ g.POST("/clientOnlineIps/:email", a.getClientOnlineIPs)
g.POST("/clientIps/:email", a.getClientIps)
g.POST("/clearClientIps/:email", a.clearClientIps)
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) {
email := c.Param("email")
diff --git a/web/controller/util.go b/web/controller/util.go
index 440de276..04084f41 100644
--- a/web/controller/util.go
+++ b/web/controller/util.go
@@ -46,8 +46,13 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
}
} else {
m.Success = false
- m.Msg = msg + " " + I18nWeb(c, "fail") + ": " + err.Error()
- logger.Warning(msg+" "+I18nWeb(c, "fail")+": ", err)
+ if msg != "" {
+ 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)
}
diff --git a/web/service/inbound.go b/web/service/inbound.go
index 4f28af21..71c0f132 100644
--- a/web/service/inbound.go
+++ b/web/service/inbound.go
@@ -1855,6 +1855,18 @@ func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.Client
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) {
db := database.GetDB()
InboundClientIps := &model.InboundClientIps{}
diff --git a/web/service/server.go b/web/service/server.go
index 21ab66f5..4c9731f5 100644
--- a/web/service/server.go
+++ b/web/service/server.go
@@ -61,6 +61,7 @@ type Status struct {
State ProcessState `json:"state"`
ErrorMsg string `json:"errorMsg"`
Version string `json:"version"`
+ ApiPort string `json:"apiPort"`
} `json:"xray"`
Uptime uint64 `json:"uptime"`
Loads []float64 `json:"loads"`
@@ -239,6 +240,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
status.Xray.ErrorMsg = s.xrayService.GetXrayResult()
}
status.Xray.Version = s.xrayService.GetXrayVersion()
+ status.Xray.ApiPort = s.xrayService.GetXrayApiPort()
var rtm runtime.MemStats
runtime.ReadMemStats(&rtm)
diff --git a/web/service/xray.go b/web/service/xray.go
index d37c963a..5998b5f0 100644
--- a/web/service/xray.go
+++ b/web/service/xray.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"sync"
+ "strconv"
"x-ui/logger"
"x-ui/xray"
@@ -56,6 +57,13 @@ func (s *XrayService) GetXrayVersion() string {
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{} {
return append(s[:index], s[index+1:]...)
}
diff --git a/xray/api.go b/xray/api.go
index 727ab526..988e3b44 100644
--- a/xray/api.go
+++ b/xray/api.go
@@ -204,6 +204,35 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
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) {
isInbound := matches[1] == "inbound"
tag := matches[2]
From 1a9c0cf875aeb1a1d3d9cb3fb6fd265e29ba17d7 Mon Sep 17 00:00:00 2001
From: serogaq <36307024+serogaq@users.noreply.github.com>
Date: Wed, 11 Dec 2024 13:31:11 +0300
Subject: [PATCH 2/5] 10 / access and error logs
---
logger/logger.go | 33 -----
web/controller/server.go | 14 +-
web/html/xui/index.html | 201 +++++++++++++++++++++------
web/service/server.go | 58 ++++++--
web/translation/translate.en_US.toml | 3 +-
web/translation/translate.es_ES.toml | 3 +-
web/translation/translate.fa_IR.toml | 3 +-
web/translation/translate.id_ID.toml | 3 +-
web/translation/translate.ja_JP.toml | 3 +-
web/translation/translate.pt_BR.toml | 3 +-
web/translation/translate.ru_RU.toml | 3 +-
web/translation/translate.tr_TR.toml | 3 +-
web/translation/translate.uk_UA.toml | 3 +-
web/translation/translate.vi_VN.toml | 3 +-
web/translation/translate.zh_CN.toml | 3 +-
web/translation/translate.zh_TW.toml | 3 +-
xray/process.go | 24 ++++
17 files changed, 261 insertions(+), 105 deletions(-)
diff --git a/logger/logger.go b/logger/logger.go
index 52b1977a..35c5c0ac 100644
--- a/logger/logger.go
+++ b/logger/logger.go
@@ -4,7 +4,6 @@ import (
"fmt"
"os"
"time"
- "strings"
"github.com/op/go-logging"
)
@@ -127,35 +126,3 @@ func GetLogs(c int, level string) []string {
}
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
-}
diff --git a/web/controller/server.go b/web/controller/server.go
index 15ccb258..35f4c688 100644
--- a/web/controller/server.go
+++ b/web/controller/server.go
@@ -45,8 +45,8 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/restartXrayService", a.restartXrayService)
g.POST("/installXray/:version", a.installXray)
g.POST("/logs/:count", a.getLogs)
- g.GET("/logs-sniffed/:count", a.getLogsSniffedDomains)
- g.GET("/logs-blocked/:count", a.getLogsBlockedDomains)
+ g.POST("/access-log/:count", a.getAccessLog)
+ g.POST("/error-log/:count", a.getErrorLog)
g.POST("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb)
g.POST("/importDB", a.importDB)
@@ -127,15 +127,17 @@ func (a *ServerController) getLogs(c *gin.Context) {
jsonObj(c, logs, nil)
}
-func (a *ServerController) getLogsSniffedDomains(c *gin.Context) {
+func (a *ServerController) getAccessLog(c *gin.Context) {
count := c.Param("count")
- logs := a.serverService.GetLogsSniffedDomains(count)
+ grep := c.PostForm("grep")
+ logs := a.serverService.GetAccessLog(count, grep)
jsonObj(c, logs, nil)
}
-func (a *ServerController) getLogsBlockedDomains(c *gin.Context) {
+func (a *ServerController) getErrorLog(c *gin.Context) {
count := c.Param("count")
- logs := a.serverService.GetLogsBlockedDomains(count)
+ grep := c.PostForm("grep")
+ logs := a.serverService.GetErrorLog(count, grep)
jsonObj(c, logs, nil)
}
diff --git a/web/html/xui/index.html b/web/html/xui/index.html
index 50902b8a..78aa1c39 100644
--- a/web/html/xui/index.html
+++ b/web/html/xui/index.html
@@ -125,7 +125,8 @@
{{ i18n "menu.link" }}:
{{ i18n "pages.index.logs" }}
- {{ i18n "pages.index.logDomains" }}
+ {{ i18n "pages.index.accessLog" }}
+ {{ i18n "pages.index.errorLog" }}
{{ i18n "pages.index.config" }}
{{ i18n "pages.index.backup" }}
@@ -315,42 +316,87 @@
- logDomainsModal.visible = false"
+ accessLogModal.visible = false"
:class="themeSwitcher.currentTheme"
width="800px" footer="">
- {{ i18n "pages.index.logDomains" }}
-
+ :disabled="accessLogModal.loading"
+ @click="openAccessLog()">
-
- Few
- Medium
- Many
-
-
- Sniffed
- Blocked
+
+ 500
+ 2500
+ 7000
+
+
+
+ :href="'data:application/text;charset=utf-8,' + encodeURIComponent(accessLogModal.logs?.join('\n'))" download="xray-access.log">
-
+
+
+ errorLogModal.visible = false"
+ :class="themeSwitcher.currentTheme"
+ width="800px" footer="">
+
+ {{ i18n "pages.index.errorLog" }}
+
+
+
+
+
+
+
+ 500
+ 2500
+ 7000
+
+
+
+
+
+
+
+
+
+
+
+
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 = '';
+ const levelColors = ["#3c89e8","#008771","#008771","#f37b24","#e04141","#bcbcbc"];
logs.forEach((log, index) => {
- let [data, message] = log.split(" - ",2);
- const parts = data.split(" ");
- if(index>0) formattedLogs += '
';
+ if (log.length <= 3) {
+ return;
+ }
+ 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 += '
';
+ formattedLogs += `${date} ${time} `;
+ formattedLogs += `- ${message}`;
+ });
- if (parts.length === 2) {
- const d = parts[0];
- const t = parts[1];
- formattedLogs += `${d} ${t}`;
+ 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 += '
';
+ 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 += '
';
+ formattedLogs += `${date} ${time} `;
+ formattedLogs += `${level} `;
+ formattedLogs += ' - ';
+
+ if (id.substr(0, 1) === '[') {
+ let idColor = idColors[idColorIndex];
+ if (lastLogId !== '' && lastLogId !== id) {
+ idColorIndex++;
+ }
+ if (idColorIndex >= idColors.length) {
+ idColorIndex = 0;
+ }
+ lastLogId = id;
+ formattedLogs += `${id} ${message}`;
} else {
- formattedLogs += `${data}`;
+ formattedLogs += `${id} ${message}`;
}
-
- if (message) {
- message = ""+(type === 'sniffed' ? 'Sniffed' : 'Blocked')+": " + message;
- }
-
- formattedLogs += message ? ' - ' + message : '';
+ }
});
return formattedLogs;
@@ -674,15 +781,25 @@
await PromiseUtil.sleep(500);
logModal.loading = false;
},
- async openLogDomains(){
- logDomainsModal.loading = true;
- const msg = await HttpUtil.get('server/logs-'+(logDomainsModal.type==='blocked'?'blocked':'sniffed')+'/'+logDomainsModal.rows);
+ async openAccessLog() {
+ accessLogModal.loading = true;
+ const msg = await HttpUtil.post('server/access-log/'+accessLogModal.rows, { grep: accessLogModal.grep });
if (!msg.success) {
return;
}
- logDomainsModal.show(msg.obj);
+ accessLogModal.show(msg.obj);
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() {
this.loading(true);
diff --git a/web/service/server.go b/web/service/server.go
index 21ab66f5..d8af59b3 100644
--- a/web/service/server.go
+++ b/web/service/server.go
@@ -448,22 +448,56 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str
return lines
}
-func (s *ServerService) GetLogsSniffedDomains(count string) []string {
- c, _ := strconv.Atoi(count)
- var lines []string
+func (s *ServerService) GetAccessLog(count string, grep string) []string {
+ accessLogPath, err := xray.GetAccessLogPath()
+ if err != nil {
+ return []string{"Error in Access Log retrieval: " + err.Error()}
+ }
- lines = logger.GetLogsSniffedDomains(c)
-
- return lines
+ if accessLogPath != "none" && accessLogPath != "" {
+ var cmdArgs []string
+ 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 {
- c, _ := strconv.Atoi(count)
- var lines []string
+func (s *ServerService) GetErrorLog(count string, grep string) []string {
+ errorLogPath, err := xray.GetErrorLogPath()
+ if err != nil {
+ return []string{"Error in Error Log retrieval: " + err.Error()}
+ }
- lines = logger.GetLogsBlockedDomains(c)
-
- return lines
+ if errorLogPath != "none" && errorLogPath != "" {
+ var cmdArgs []string
+ 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) {
diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml
index 3fae7c5e..a6b661b2 100644
--- a/web/translation/translate.en_US.toml
+++ b/web/translation/translate.en_US.toml
@@ -105,7 +105,8 @@
"xraySwitchVersionDialogDesc" = "Are you sure you want to change the Xray version to"
"dontRefresh" = "Installation is in progress, please do not refresh this page"
"logs" = "Logs"
-"logDomains" = "Log Domains"
+"accessLog" = "Access Log"
+"errorLog" = "Error Log"
"config" = "Config"
"backup" = "Backup & Restore"
"backupTitle" = "Database Backup & Restore"
diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml
index 170fedac..27928bcc 100644
--- a/web/translation/translate.es_ES.toml
+++ b/web/translation/translate.es_ES.toml
@@ -105,7 +105,8 @@
"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."
"logs" = "Registros"
-"logDomains" = "Log Domains"
+"accessLog" = "Access Log"
+"errorLog" = "Error Log"
"config" = "Configuración"
"backup" = "Copia de Seguridad y Restauración"
"backupTitle" = "Copia de Seguridad y Restauración de la Base de Datos"
diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml
index 09023f58..bf2bd972 100644
--- a/web/translation/translate.fa_IR.toml
+++ b/web/translation/translate.fa_IR.toml
@@ -105,7 +105,8 @@
"xraySwitchVersionDialogDesc" = "آیا از تغییر نسخه مطمئن هستید؟"
"dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید"
"logs" = "گزارشها"
-"logDomains" = "Log Domains"
+"accessLog" = "Access Log"
+"errorLog" = "Error Log"
"config" = "پیکربندی"
"backup" = "پشتیبانگیری"
"backupTitle" = "پشتیبانگیری دیتابیس"
diff --git a/web/translation/translate.id_ID.toml b/web/translation/translate.id_ID.toml
index f291a0d6..e635f586 100644
--- a/web/translation/translate.id_ID.toml
+++ b/web/translation/translate.id_ID.toml
@@ -105,7 +105,8 @@
"xraySwitchVersionDialogDesc" = "Apakah Anda yakin ingin mengubah versi Xray menjadi"
"dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini"
"logs" = "Log"
-"logDomains" = "Log Domains"
+"accessLog" = "Access Log"
+"errorLog" = "Error Log"
"config" = "Konfigurasi"
"backup" = "Cadangan & Pulihkan"
"backupTitle" = "Cadangan & Pulihkan Database"
diff --git a/web/translation/translate.ja_JP.toml b/web/translation/translate.ja_JP.toml
index c374a769..17beb6c4 100644
--- a/web/translation/translate.ja_JP.toml
+++ b/web/translation/translate.ja_JP.toml
@@ -105,7 +105,8 @@
"xraySwitchVersionDialogDesc" = "Xrayのバージョンを切り替えますか?"
"dontRefresh" = "インストール中、このページをリロードしないでください"
"logs" = "ログ"
-"logDomains" = "Log Domains"
+"accessLog" = "Access Log"
+"errorLog" = "Error Log"
"config" = "設定"
"backup" = "バックアップと復元"
"backupTitle" = "データベースのバックアップと復元"
diff --git a/web/translation/translate.pt_BR.toml b/web/translation/translate.pt_BR.toml
index 6532cfa4..7718e700 100644
--- a/web/translation/translate.pt_BR.toml
+++ b/web/translation/translate.pt_BR.toml
@@ -105,7 +105,8 @@
"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"
"logs" = "Logs"
-"logDomains" = "Log Domains"
+"accessLog" = "Access Log"
+"errorLog" = "Error Log"
"config" = "Configuração"
"backup" = "Backup e Restauração"
"backupTitle" = "Backup e Restauração do Banco de Dados"
diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml
index f91a06e5..aad4cd29 100644
--- a/web/translation/translate.ru_RU.toml
+++ b/web/translation/translate.ru_RU.toml
@@ -105,7 +105,8 @@
"xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?"
"dontRefresh" = "Идёт установка. Пожалуйста, не обновляйте эту страницу"
"logs" = "Логи"
-"logDomains" = "Логи доменов"
+"accessLog" = "Access Лог"
+"errorLog" = "Error Лог"
"config" = "Конфигурация"
"backup" = "Бэкап и восстановление"
"backupTitle" = "База данных бэкапа и восстановления"
diff --git a/web/translation/translate.tr_TR.toml b/web/translation/translate.tr_TR.toml
index 35180f8e..b5a6c5ee 100644
--- a/web/translation/translate.tr_TR.toml
+++ b/web/translation/translate.tr_TR.toml
@@ -105,7 +105,8 @@
"xraySwitchVersionDialogDesc" = "Xray sürümünü değiştirmek istediğinizden emin misiniz"
"dontRefresh" = "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin"
"logs" = "Günlükler"
-"logDomains" = "Log Domains"
+"accessLog" = "Access Log"
+"errorLog" = "Error Log"
"config" = "Yapılandırma"
"backup" = "Yedekle & Geri Yükle"
"backupTitle" = "Veritabanı Yedekleme & Geri Yükleme"
diff --git a/web/translation/translate.uk_UA.toml b/web/translation/translate.uk_UA.toml
index 4398985a..875aa7cb 100644
--- a/web/translation/translate.uk_UA.toml
+++ b/web/translation/translate.uk_UA.toml
@@ -105,7 +105,8 @@
"xraySwitchVersionDialogDesc" = "Ви впевнені, що бажаєте змінити версію Xray на"
"dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку"
"logs" = "Журнали"
-"logDomains" = "Логи доменов"
+"accessLog" = "Access Лог"
+"errorLog" = "Error Лог"
"config" = "Конфігурація"
"backup" = "Резервне копіювання та відновлення"
"backupTitle" = "Резервне копіювання та відновлення бази даних"
diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml
index cbaa2c3e..01a7c9d6 100644
--- a/web/translation/translate.vi_VN.toml
+++ b/web/translation/translate.vi_VN.toml
@@ -105,7 +105,8 @@
"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."
"logs" = "Nhật ký"
-"logDomains" = "Log Domains"
+"accessLog" = "Access Log"
+"errorLog" = "Error Log"
"config" = "Cấu hình"
"backup" = "Sao lưu & Khôi phục"
"backupTitle" = "Sao lưu & Khôi phục Cơ sở dữ liệu"
diff --git a/web/translation/translate.zh_CN.toml b/web/translation/translate.zh_CN.toml
index 9be53d36..c8e4922a 100644
--- a/web/translation/translate.zh_CN.toml
+++ b/web/translation/translate.zh_CN.toml
@@ -105,7 +105,8 @@
"xraySwitchVersionDialogDesc" = "是否切换 Xray 版本至"
"dontRefresh" = "安装中,请勿刷新此页面"
"logs" = "日志"
-"logDomains" = "Log Domains"
+"accessLog" = "Access Log"
+"errorLog" = "Error Log"
"config" = "配置"
"backup" = "备份和恢复"
"backupTitle" = "备份和恢复数据库"
diff --git a/web/translation/translate.zh_TW.toml b/web/translation/translate.zh_TW.toml
index 8d70a010..1f356803 100644
--- a/web/translation/translate.zh_TW.toml
+++ b/web/translation/translate.zh_TW.toml
@@ -105,7 +105,8 @@
"xraySwitchVersionDialogDesc" = "是否切換 Xray 版本至"
"dontRefresh" = "安裝中,請勿重新整理此頁面"
"logs" = "日誌"
-"logDomains" = "Log Domains"
+"accessLog" = "Access Log"
+"errorLog" = "Error Log"
"config" = "配置"
"backup" = "備份和恢復"
"backupTitle" = "備份和恢復資料庫"
diff --git a/xray/process.go b/xray/process.go
index b4947864..7e1d00d1 100644
--- a/xray/process.go
+++ b/xray/process.go
@@ -81,6 +81,30 @@ func GetAccessLogPath() (string, error) {
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) {
p.Stop()
}
From f3b7e3cc6df7f0e56ccda8cadd35795296111c18 Mon Sep 17 00:00:00 2001
From: serogaq <36307024+serogaq@users.noreply.github.com>
Date: Wed, 11 Dec 2024 17:15:34 +0300
Subject: [PATCH 3/5] 11 / getRemoteIp - priorityHeader
11 / getRemoteIp - priorityHeader from env
---
.env.example | 3 ++-
docker-compose.yml | 1 +
web/controller/util.go | 15 ++++++++++++++-
3 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/.env.example b/.env.example
index 92a85eba..e52ce84f 100644
--- a/.env.example
+++ b/.env.example
@@ -9,4 +9,5 @@ XUI_VLESS_SNI=""
#XUI_SUB_SUPPORT_URL=""
#XUI_SUB_PROFILE_WEB_PAGE_URL=""
#XUI_DEBUG="false"
-#XUI_LOG_LEVEL="info"
\ No newline at end of file
+#XUI_LOG_LEVEL="info"
+#XUI_GETREMOTEIP_PRIORITY_HEADER=""
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 681d1fa2..886b14c7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -51,6 +51,7 @@ services:
XUI_SUB_PROFILE_TITLE: "${XUI_SUB_PROFILE_TITLE:-}"
XUI_SUB_SUPPORT_URL: "${XUI_SUB_SUPPORT_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_LOG_LEVEL: "${XUI_LOG_LEVEL:-info}"
tty: true
diff --git a/web/controller/util.go b/web/controller/util.go
index 04084f41..aa92e12a 100644
--- a/web/controller/util.go
+++ b/web/controller/util.go
@@ -3,6 +3,7 @@ package controller
import (
"net"
"net/http"
+ "os"
"strings"
"x-ui/config"
@@ -13,7 +14,19 @@ import (
)
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 != "" {
return value
}
From 8aabe1b049d94b201177589825cf99ae9106aac0 Mon Sep 17 00:00:00 2001
From: serogaq <36307024+serogaq@users.noreply.github.com>
Date: Thu, 12 Dec 2024 00:29:21 +0300
Subject: [PATCH 4/5] feature / 10
10 / fix
---
web/html/xui/index.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/html/xui/index.html b/web/html/xui/index.html
index 78aa1c39..75758c5b 100644
--- a/web/html/xui/index.html
+++ b/web/html/xui/index.html
@@ -654,13 +654,13 @@
formattedLogs += ' - ';
if (id.substr(0, 1) === '[') {
- let idColor = idColors[idColorIndex];
if (lastLogId !== '' && lastLogId !== id) {
idColorIndex++;
}
if (idColorIndex >= idColors.length) {
idColorIndex = 0;
}
+ let idColor = idColors[idColorIndex];
lastLogId = id;
formattedLogs += `${id} ${message}`;
} else {
From 377a6b678679e78f4ca36475e8d53afc40f6d591 Mon Sep 17 00:00:00 2001
From: serogaq <36307024+serogaq@users.noreply.github.com>
Date: Fri, 13 Dec 2024 01:43:33 +0300
Subject: [PATCH 5/5] feature / 12
12 / log xray api port
12 / getXuiLatestVersion
12 / caching
---
caching/caching.go | 52 +++++++++++++++++++++++++++++++
go.mod | 1 +
go.sum | 2 ++
main.go | 28 ++++++++++++++++-
web/global/global.go | 15 +++++++++
web/service/server.go | 71 ++++++++++++++++++++++++++++++++++++++++---
web/service/xray.go | 4 +++
7 files changed, 168 insertions(+), 5 deletions(-)
create mode 100644 caching/caching.go
diff --git a/caching/caching.go b/caching/caching.go
new file mode 100644
index 00000000..18a2930a
--- /dev/null
+++ b/caching/caching.go
@@ -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
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 63f4f64e..0bc60bec 100644
--- a/go.mod
+++ b/go.mod
@@ -59,6 +59,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // 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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
diff --git a/go.sum b/go.sum
index 55f92f4b..b54f80ca 100644
--- a/go.sum
+++ b/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/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/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/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
diff --git a/main.go b/main.go
index 84ffca6e..1fd7cee8 100644
--- a/main.go
+++ b/main.go
@@ -11,6 +11,7 @@ import (
"x-ui/config"
"x-ui/database"
+ "x-ui/caching"
"x-ui/logger"
"x-ui/sub"
"x-ui/web"
@@ -61,6 +62,17 @@ func runWebServer() {
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)
// Trap shutdown signals
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
@@ -79,6 +91,10 @@ func runWebServer() {
if err != nil {
logger.Debug("Error stopping sub server:", err)
}
+ err = cacheInstance.Flush()
+ if err != nil {
+ logger.Debug("Error clearing cache:", err)
+ }
server = web.NewServer()
global.SetWebServer(server)
@@ -98,10 +114,20 @@ func runWebServer() {
}
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:
server.Stop()
subServer.Stop()
- log.Println("Shutting down servers.")
+ cacheInstance.Flush()
+ log.Println("Shutting down servers and cache clearing.")
return
}
}
diff --git a/web/global/global.go b/web/global/global.go
index e92c375b..f9d04dab 100644
--- a/web/global/global.go
+++ b/web/global/global.go
@@ -5,11 +5,13 @@ import (
_ "unsafe"
"github.com/robfig/cron/v3"
+ "github.com/patrickmn/go-cache"
)
var (
webServer WebServer
subServer SubServer
+ caching Cache
)
type WebServer interface {
@@ -21,6 +23,11 @@ type SubServer interface {
GetCtx() context.Context
}
+type Cache interface {
+ Memory() *cache.Cache
+ GetCtx() context.Context
+}
+
func SetWebServer(s WebServer) {
webServer = s
}
@@ -36,3 +43,11 @@ func SetSubServer(s SubServer) {
func GetSubServer() SubServer {
return subServer
}
+
+func SetCache(c Cache) {
+ caching = c
+}
+
+func GetCache() Cache {
+ return caching
+}
diff --git a/web/service/server.go b/web/service/server.go
index 73aadf57..e82e0cd1 100644
--- a/web/service/server.go
+++ b/web/service/server.go
@@ -15,6 +15,7 @@ import (
"strconv"
"strings"
"time"
+ "regexp"
"x-ui/config"
"x-ui/database"
@@ -22,6 +23,7 @@ import (
"x-ui/util/common"
"x-ui/util/sys"
"x-ui/xray"
+ "x-ui/web/global"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/disk"
@@ -61,8 +63,10 @@ type Status struct {
State ProcessState `json:"state"`
ErrorMsg string `json:"errorMsg"`
Version string `json:"version"`
- ApiPort string `json:"apiPort"`
} `json:"xray"`
+ XUI struct {
+ LatestVersion string `json:"latestVersion"`
+ } `json:"xui"`
Uptime uint64 `json:"uptime"`
Loads []float64 `json:"loads"`
TcpCount int `json:"tcpCount"`
@@ -95,6 +99,14 @@ type ServerService struct {
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 {
var host string
host = os.Getenv("XUI_SERVER_IP")
@@ -121,7 +133,37 @@ func getPublicIP(url string) string {
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 {
+ cache := global.GetCache().Memory()
+
now := time.Now()
status := &Status{
T: now,
@@ -224,8 +266,27 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
logger.Warning("get udp connections failed:", err)
}
- status.PublicIP.IPv4 = getPublicIP("https://api.ipify.org")
- status.PublicIP.IPv6 = getPublicIP("https://api6.ipify.org")
+ 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")
+ 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() {
status.Xray.State = Running
@@ -240,7 +301,9 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
status.Xray.ErrorMsg = s.xrayService.GetXrayResult()
}
status.Xray.Version = s.xrayService.GetXrayVersion()
- status.Xray.ApiPort = s.xrayService.GetXrayApiPort()
+
+ status.XUI.LatestVersion = getXuiLatestVersion()
+
var rtm runtime.MemStats
runtime.ReadMemStats(&rtm)
diff --git a/web/service/xray.go b/web/service/xray.go
index 5998b5f0..a910f9ed 100644
--- a/web/service/xray.go
+++ b/web/service/xray.go
@@ -213,6 +213,10 @@ func (s *XrayService) RestartXray(isForce bool) error {
if err != nil {
return err
}
+ if isForce {
+ logger.Debug("Xray Api Port: ", strconv.Itoa(p.GetAPIPort()))
+ }
+
return nil
}