Browse Source

Added xray access log viewer (#3309)

* added xray access log viewer

* made modal window width adaptive

* hide logs button if xray logs are disabled
fgsfds 2 weeks ago
parent
commit
957f3dbb54
3 changed files with 151 additions and 2 deletions
  1. 7 0
      web/controller/server.go
  2. 112 2
      web/html/index.html
  3. 32 0
      web/service/server.go

+ 7 - 0
web/controller/server.go

@@ -46,6 +46,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
 	g.POST("/installXray/:version", a.installXray)
 	g.POST("/updateGeofile/:fileName", a.updateGeofile)
 	g.POST("/logs/:count", a.getLogs)
+	g.POST("/xraylogs/:count", a.getXrayLogs)
 	g.POST("/getConfigJson", a.getConfigJson)
 	g.GET("/getDb", a.getDb)
 	g.POST("/importDB", a.importDB)
@@ -134,6 +135,12 @@ func (a *ServerController) getLogs(c *gin.Context) {
 	jsonObj(c, logs, nil)
 }
 
+func (a *ServerController) getXrayLogs(c *gin.Context) {
+	count := c.Param("count")
+	logs := a.serverService.GetXrayLogs(count)
+	jsonObj(c, logs, nil)
+}
+
 func (a *ServerController) getConfigJson(c *gin.Context) {
 	configJson, err := a.serverService.GetConfigJson()
 	if err != nil {

+ 112 - 2
web/html/index.html

@@ -167,7 +167,10 @@
                               <span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
                             </a-col>
                             <a-col>
-                              <a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-tag>
+                              <a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-icon>
+                            </a-col>
+                            <a-col>
+                              <a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openXrayLogs()"></a-icon>
                             </a-col>
                           </a-row>
                         </span>
@@ -179,6 +182,10 @@
                     </template>
                   </template>
                   <template #actions>
+                    <a-space v-if="app.ipLimitEnable" direction="horizontal" @click="openXrayLogs()" :style="{ justifyContent: 'center' }">
+                      <a-icon type="bars"></a-icon>
+                      <span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
+                    </a-space>
                     <a-space direction="horizontal" @click="stopXrayService" :style="{ justifyContent: 'center' }">
                       <a-icon type="poweroff"></a-icon>
                       <span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
@@ -422,6 +429,40 @@
     </a-form>
     <div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="logModal.formattedLogs"></div>
   </a-modal>
+  <a-modal id="xraylog-modal"
+      v-model="xraylogModal.visible"
+      :closable="true" @cancel="() => xraylogModal.visible = false"
+      :class="themeSwitcher.currentTheme"
+      width="80vw"
+      footer="">
+    <template slot="title">
+      {{ i18n "pages.index.logs" }}
+      <a-icon :spin="xraylogModal.loading"
+        type="sync"
+        :style="{ verticalAlign: 'middle', marginLeft: '10px' }"
+        :disabled="xraylogModal.loading"
+        @click="openXrayLogs()">
+      </a-icon>
+    </template>
+    <a-form layout="inline">
+      <a-form-item :style="{ marginRight: '0.5rem' }">
+        <a-input-group compact>
+          <a-select size="small" v-model="xraylogModal.rows" :style="{ width: '70px' }"
+              @change="openXrayLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
+            <a-select-option value="10">10</a-select-option>
+            <a-select-option value="20">20</a-select-option>
+            <a-select-option value="50">50</a-select-option>
+            <a-select-option value="100">100</a-select-option>
+            <a-select-option value="500">500</a-select-option>
+          </a-select>
+        </a-input-group>
+      </a-form-item>
+      <a-form-item :style="{ float: 'right' }">
+        <a-button type="primary" icon="download" @click="FileManager.downloadTextFile(xraylogModal.logs?.join('\n'), 'x-ui.log')"></a-button>
+      </a-form-item>
+    </a-form>
+    <div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="xraylogModal.formattedLogs"></div>
+  </a-modal>
   <a-modal id="backup-modal" 
       v-model="backupModal.visible" 
       title='{{ i18n "pages.index.backupTitle" }}'
@@ -606,6 +647,57 @@
         },
     };
 
+    const xraylogModal = {
+        visible: false,
+        logs: [],
+        rows: 20,
+        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 = '';
+
+          logs.forEach((log, index) => {
+            if(index > 0) formattedLogs += '<br>';
+
+            const parts = log.split(' ');
+
+            if(parts.length === 9) {
+              const dateTime = `<b>${parts[0]} ${parts[1]}</b>`;
+              const from = `<b>${parts[3]}</b>`;
+              const to = `<b>${parts[5].replace(/^\/+/, "")}</b>`;
+
+              let outboundColor = '';
+              if (parts[8].startsWith('blocked')) {
+                outboundColor = ' style="color: #e04141;"';
+              }
+              else if (!parts[8].startsWith('direct')) {
+                outboundColor = ' style="color: #3c89e8;"';
+              }
+
+              formattedLogs += `<span${outboundColor}>
+${dateTime}
+ ${parts[2]}
+ ${from}
+ ${parts[4]}
+ ${to}
+ ${parts.slice(6).join(' ')}
+</span>`;
+            } else {
+              formattedLogs += `<span>${parts.join(' ')}</span>`;
+            }
+          });
+
+            return formattedLogs;
+        },
+        hide() {
+            this.visible = false;
+        },
+    };
+
     const backupModal = {
         visible: false,
         show() {
@@ -629,10 +721,12 @@
             status: new Status(),
             versionModal,
             logModal,
+            xraylogModal,
             backupModal,
             loadingTip: '{{ i18n "loading"}}',
             showAlert: false,
-            showIp: false
+            showIp: false,
+            ipLimitEnable: false,
         },
         methods: {
             loading(spinning, tip = '{{ i18n "loading"}}') {
@@ -721,6 +815,16 @@
                 await PromiseUtil.sleep(500);
                 logModal.loading = false;
             },
+            async openXrayLogs(){
+              xraylogModal.loading = true;
+                const msg = await HttpUtil.post('server/xraylogs/'+xraylogModal.rows);
+                if (!msg.success) {
+                    return;
+                }
+              xraylogModal.show(msg.obj);
+                await PromiseUtil.sleep(500);
+              xraylogModal.loading = false;
+            },
             async openConfig() {
                 this.loading(true);
                 const msg = await HttpUtil.post('server/getConfigJson');
@@ -773,6 +877,12 @@
             if (window.location.protocol !== "https:") {
                 this.showAlert = true;
             }
+
+            const msg = await HttpUtil.post('/panel/setting/defaultSettings');
+            if (msg.success) {
+              this.ipLimitEnable = msg.obj.ipLimitEnable;
+            }
+
             while (true) {
                 try {
                     await this.getStatus();

+ 32 - 0
web/service/server.go

@@ -2,6 +2,7 @@ package service
 
 import (
 	"archive/zip"
+	"bufio"
 	"bytes"
 	"encoding/json"
 	"fmt"
@@ -481,6 +482,37 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str
 	return lines
 }
 
+func (s *ServerService) GetXrayLogs(count string) []string {
+	c, _ := strconv.Atoi(count)
+	var lines []string
+
+	pathToAccessLog, err := xray.GetAccessLogPath()
+	if err != nil {
+		return lines
+	}
+
+	file, err := os.Open(pathToAccessLog)
+	if err != nil {
+		return lines
+	}
+	defer file.Close()
+
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		line := scanner.Text()
+		if strings.TrimSpace(line) == "" || strings.Contains(line, "api -> api") {
+			continue
+		}
+		lines = append(lines, line)
+	}
+
+	if len(lines) > c {
+		lines = lines[len(lines)-c:]
+	}
+
+	return lines
+}
+
 func (s *ServerService) GetConfigJson() (any, error) {
 	config, err := s.xrayService.GetXrayConfig()
 	if err != nil {