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 @@
- - - Few - Medium - Many - - - Sniffed - Blocked + + 500 + 2500 + 7000 + + + + :href="'data:application/text;charset=utf-8,' + encodeURIComponent(accessLogModal.logs?.join('\n'))" download="xray-access.log"> -
+
+
+ + + + + + + 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 }