mirror of
				https://github.com/MHSanaei/3x-ui.git
				synced 2025-10-27 10:30:08 +00:00 
			
		
		
		
	Compare commits
	
		
			4 commits
		
	
	
		
			01eb713019
			...
			10c80fd0b9
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 10c80fd0b9 | ||
|   | b3daf2940d | ||
|   | 8b8c7aac9b | ||
|   | 2eb8abf61e | 
					 4 changed files with 216 additions and 69 deletions
				
			
		
							
								
								
									
										2
									
								
								web/assets/css/custom.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								web/assets/css/custom.min.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -343,7 +343,7 @@ | |||
|     <a-form layout="inline"> | ||||
|       <a-form-item class="mr-05"> | ||||
|         <a-input-group compact> | ||||
|           <a-select size="small" v-model="logModal.rows" class="w-70" @change="openLogs()" | ||||
|           <a-select size="small" v-model="logModal.rows" :style="{ width: '70px' }" @change="openLogs()" | ||||
|             :dropdown-class-name="themeSwitcher.currentTheme"> | ||||
|             <a-select-option value="10">10</a-select-option> | ||||
|             <a-select-option value="20">20</a-select-option> | ||||
|  | @ -351,7 +351,7 @@ | |||
|             <a-select-option value="100">100</a-select-option> | ||||
|             <a-select-option value="500">500</a-select-option> | ||||
|           </a-select> | ||||
|           <a-select size="small" v-model="logModal.level" class="w-95" @change="openLogs()" | ||||
|           <a-select size="small" v-model="logModal.level" :style="{ width: '95px' }" @change="openLogs()" | ||||
|             :dropdown-class-name="themeSwitcher.currentTheme"> | ||||
|             <a-select-option value="debug">Debug</a-select-option> | ||||
|             <a-select-option value="info">Info</a-select-option> | ||||
|  | @ -365,8 +365,7 @@ | |||
|         <a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox> | ||||
|       </a-form-item> | ||||
|       <a-form-item style="float: right;"> | ||||
|         <a-button type="primary" icon="download" | ||||
|           @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button> | ||||
|         <a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
|     <div class="ant-input log-container" v-html="logModal.formattedLogs"></div> | ||||
|  | @ -382,7 +381,7 @@ | |||
|     <a-form layout="inline"> | ||||
|       <a-form-item class="mr-05"> | ||||
|         <a-input-group compact> | ||||
|           <a-select size="small" v-model="xraylogModal.rows" class="w-70" @change="openXrayLogs()" | ||||
|           <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> | ||||
|  | @ -401,8 +400,7 @@ | |||
|         <a-checkbox v-model="xraylogModal.showProxy" @change="openXrayLogs()">Proxy</a-checkbox> | ||||
|       </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-button type="primary" icon="download" @click="downloadXrayLogs"></a-button> | ||||
|       </a-form-item> | ||||
|     </a-form> | ||||
|     <div class="ant-input log-container" v-html="xraylogModal.formattedLogs"></div> | ||||
|  | @ -796,59 +794,74 @@ | |||
|   }; | ||||
| 
 | ||||
|   const xraylogModal = { | ||||
|     visible: false, | ||||
|     logs: [], | ||||
|     rows: 20, | ||||
|     showDirect: true, | ||||
|     showBlocked: true, | ||||
|     showProxy: true, | ||||
|     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 = ''; | ||||
|       visible: false, | ||||
|       logs: [], | ||||
|       rows: 20, | ||||
|       showDirect: true, | ||||
|       showBlocked: true, | ||||
|       showProxy: true, | ||||
|       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 = ` | ||||
| <style> | ||||
|   table { | ||||
|     border-collapse: collapse; | ||||
|     width: auto; | ||||
|   } | ||||
| 
 | ||||
|       logs.forEach((log, index) => { | ||||
|         if (index > 0) formattedLogs += '<br>'; | ||||
|   table td, table th { | ||||
|     padding: 2px 15px; | ||||
|   } | ||||
| </style> | ||||
| 
 | ||||
|         const parts = log.split(' '); | ||||
| 
 | ||||
|         if (parts.length === 10) { | ||||
|           const dateTime = `<b>${parts[0]} ${parts[1]}</b>`; | ||||
|           const from = `<b>${parts[3]}</b>`; | ||||
|           const to = `<b>${parts[5].replace(/^\/+/, "")}</b>`; | ||||
| <table> | ||||
|     <tr> | ||||
|         <th>Date</th> | ||||
|         <th>From</th> | ||||
|         <th>To</th> | ||||
|         <th>Inbound</th> | ||||
|         <th>Outbound</th> | ||||
|         <th>Email</th> | ||||
|     </tr> | ||||
| `; | ||||
| 
 | ||||
|         logs.reverse().forEach((log, index) => { | ||||
|           let outboundColor = ''; | ||||
|           if (parts[9] === "b") { | ||||
|           if (log.Event === 1) { | ||||
|             outboundColor = ' style="color: #e04141;"'; //red for blocked | ||||
|           } | ||||
|           else if (parts[9] === "p") { | ||||
|           else if (log.Event === 2) { | ||||
|             outboundColor = ' style="color: #3c89e8;"'; //blue for proxies | ||||
|           } | ||||
| 
 | ||||
|           formattedLogs += `<span${outboundColor}> | ||||
| ${dateTime} | ||||
|  ${parts[2]} | ||||
|  ${from} | ||||
|  ${parts[4]} | ||||
|  ${to} | ||||
|  ${parts.slice(6, 9).join(' ')} | ||||
| </span>`; | ||||
|         } else { | ||||
|           formattedLogs += `<span>${log}</span>`; | ||||
|         } | ||||
|       }); | ||||
|           let text = ``; | ||||
|           if (log.Email !== "") { | ||||
|             text = `<td>${log.Email}</td>`; | ||||
|           } | ||||
| 
 | ||||
|       return formattedLogs; | ||||
|     }, | ||||
|     hide() { | ||||
|       this.visible = false; | ||||
|     }, | ||||
|   }; | ||||
|           formattedLogs += ` | ||||
| <tr ${outboundColor}> | ||||
|     <td><b>${new Date(log.DateTime).toLocaleString()}</b></td> | ||||
|     <td>${log.FromAddress}</td> | ||||
|     <td>${log.ToAddress}</td> | ||||
|     <td>${log.Inbound}</td> | ||||
|     <td>${log.Outbound}</td> | ||||
|     ${text} | ||||
| </tr> | ||||
| `; | ||||
|         }); | ||||
| 
 | ||||
|         return formattedLogs += "</table>"; | ||||
|       }, | ||||
|       hide() { | ||||
|         this.visible = false; | ||||
|       }, | ||||
|     }; | ||||
|   const backupModal = { | ||||
|     visible: false, | ||||
|     show() { | ||||
|  | @ -1023,6 +1036,25 @@ ${dateTime} | |||
|         await PromiseUtil.sleep(500); | ||||
|         xraylogModal.loading = false; | ||||
|       }, | ||||
|       downloadXrayLogs() { | ||||
|         if (!Array.isArray(this.xraylogModal.logs) || this.xraylogModal.logs.length === 0) { | ||||
|           FileManager.downloadTextFile('', 'x-ui.log'); | ||||
|           return; | ||||
|         } | ||||
|         const lines = this.xraylogModal.logs.map(l => { | ||||
|           try { | ||||
|             const dt = l.DateTime ? new Date(l.DateTime) : null; | ||||
|             const dateStr = dt && !isNaN(dt.getTime()) ? dt.toISOString() : ''; | ||||
|             const eventMap = { 0: 'DIRECT', 1: 'BLOCKED', 2: 'PROXY' }; | ||||
|             const eventText = eventMap[l.Event] || String(l.Event ?? ''); | ||||
|             const emailPart = l.Email ? ` Email=${l.Email}` : ''; | ||||
|             return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`.trim(); | ||||
|           } catch (e) { | ||||
|             return JSON.stringify(l); | ||||
|           } | ||||
|         }).join('\n'); | ||||
|         FileManager.downloadTextFile(lines, 'x-ui.log'); | ||||
|       }, | ||||
|       async openConfig() { | ||||
|         this.loading(true); | ||||
|         const msg = await HttpUtil.get('/panel/api/server/getConfigJson'); | ||||
|  | @ -1071,7 +1103,6 @@ ${dateTime} | |||
|         fileInput.click(); | ||||
|       }, | ||||
|     }, | ||||
|     computed: {}, | ||||
|     async mounted() { | ||||
|       if (window.location.protocol !== "https:") { | ||||
|         this.showAlert = true; | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ | |||
| {{ template "page/body_start" .}} | ||||
| <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'"> | ||||
|   <transition name="list" appear> | ||||
|   <a-layout-content class="under min-h-100vh"> | ||||
|   <a-layout-content class="under min-h-0"> | ||||
|       <div class="waves-header"> | ||||
|         <div class="waves-inner-header"></div> | ||||
|         <svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" | ||||
|  | @ -20,7 +20,7 @@ | |||
|           </g> | ||||
|         </svg> | ||||
|       </div> | ||||
|   <a-row type="flex" justify="center" align="middle" class="h-100 overflow-hidden-auto"> | ||||
|   <a-row type="flex" justify="center" align="middle" class="h-100 overflow-y-auto overflow-x-hidden"> | ||||
|         <a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" class="my-3rem"> | ||||
|           <template v-if="!loadingStates.fetched"> | ||||
|             <div class="text-center"> | ||||
|  | @ -184,5 +184,80 @@ | |||
|       newWord.classList.add('is-visible'); | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   const pm_input_selector = 'input.ant-input, textarea.ant-input'; | ||||
|   const pm_strip_props = [ | ||||
|     'background', | ||||
|     'background-color', | ||||
|     'background-image', | ||||
|     'color' | ||||
|   ]; | ||||
| 
 | ||||
|   const pm_observed_forms = new WeakSet(); | ||||
| 
 | ||||
|   function pm_strip_inline(el) { | ||||
|     if (!el || el.nodeType !== 1 || !el.matches?.(pm_input_selector)) return; | ||||
| 
 | ||||
|     let did_change = false; | ||||
|     for (const prop of pm_strip_props) { | ||||
|       if (el.style.getPropertyValue(prop)) { | ||||
|         el.style.removeProperty(prop); | ||||
|         did_change = true; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (did_change && el.style.length === 0) { | ||||
|       el.removeAttribute('style'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function pm_attach_observer(form) { | ||||
|     if (pm_observed_forms.has(form)) return; | ||||
|     pm_observed_forms.add(form); | ||||
| 
 | ||||
|     form.querySelectorAll(pm_input_selector).forEach(pm_strip_inline); | ||||
| 
 | ||||
|     const pm_mo = new MutationObserver(mutations => { | ||||
|       for (const m of mutations) { | ||||
|         if (m.type === 'attributes') { | ||||
|           pm_strip_inline(m.target); | ||||
|         } else if (m.type === 'childList') { | ||||
|           for (const n of m.addedNodes) { | ||||
|             if (n.nodeType !== 1) continue; | ||||
|             if (n.matches?.(pm_input_selector)) pm_strip_inline(n); | ||||
|             n.querySelectorAll?.(pm_input_selector).forEach(pm_strip_inline); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     pm_mo.observe(form, { | ||||
|       attributes: true, | ||||
|       attributeFilter: ['style'], | ||||
|       childList: true, | ||||
|       subtree: true | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function pm_init() { | ||||
|     document.querySelectorAll('form.ant-form').forEach(pm_attach_observer); | ||||
|     const pm_host = document.getElementById('login') || document.body; | ||||
|     const pm_wait_for_forms = new MutationObserver(mutations => { | ||||
|       for (const m of mutations) { | ||||
|         for (const n of m.addedNodes) { | ||||
|           if (n.nodeType !== 1) continue; | ||||
|           if (n.matches?.('form.ant-form')) pm_attach_observer(n); | ||||
|           n.querySelectorAll?.('form.ant-form').forEach(pm_attach_observer); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|     pm_wait_for_forms.observe(pm_host, { childList: true, subtree: true }); | ||||
|   } | ||||
| 
 | ||||
|   if (document.readyState === 'loading') { | ||||
|     document.addEventListener('DOMContentLoaded', pm_init, { once: true }); | ||||
|   } else { | ||||
|     pm_init(); | ||||
|   } | ||||
| </script> | ||||
| {{ template "page/body_end" .}} | ||||
|  | @ -174,6 +174,16 @@ type CPUSample struct { | |||
| 	Cpu float64 `json:"cpu"` // percent 0..100
 | ||||
| } | ||||
| 
 | ||||
| type LogEntry struct { | ||||
| 	DateTime    time.Time | ||||
| 	FromAddress string | ||||
| 	ToAddress   string | ||||
| 	Inbound     string | ||||
| 	Outbound    string | ||||
| 	Email       string | ||||
| 	Event       int | ||||
| } | ||||
| 
 | ||||
| func getPublicIP(url string) string { | ||||
| 	client := &http.Client{ | ||||
| 		Timeout: 3 * time.Second, | ||||
|  | @ -704,19 +714,25 @@ func (s *ServerService) GetXrayLogs( | |||
| 	showBlocked string, | ||||
| 	showProxy string, | ||||
| 	freedoms []string, | ||||
| 	blackholes []string) []string { | ||||
| 	blackholes []string) []LogEntry { | ||||
| 
 | ||||
| 	const ( | ||||
| 		Direct = iota | ||||
| 		Blocked | ||||
| 		Proxied | ||||
| 	) | ||||
| 
 | ||||
| 	countInt, _ := strconv.Atoi(count) | ||||
| 	var lines []string | ||||
| 	var entries []LogEntry | ||||
| 
 | ||||
| 	pathToAccessLog, err := xray.GetAccessLogPath() | ||||
| 	if err != nil { | ||||
| 		return lines | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	file, err := os.Open(pathToAccessLog) | ||||
| 	if err != nil { | ||||
| 		return lines | ||||
| 		return nil | ||||
| 	} | ||||
| 	defer file.Close() | ||||
| 
 | ||||
|  | @ -735,37 +751,62 @@ func (s *ServerService) GetXrayLogs( | |||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		//adding suffixes to further distinguish entries by outbound
 | ||||
| 		if hasSuffix(line, freedoms) { | ||||
| 		var entry LogEntry | ||||
| 		parts := strings.Fields(line) | ||||
| 
 | ||||
| 		for i, part := range parts { | ||||
| 
 | ||||
| 			if i == 0 { | ||||
| 				dateTime, err := time.Parse("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1]) | ||||
| 				if err != nil { | ||||
| 					continue | ||||
| 				} | ||||
| 				entry.DateTime = dateTime | ||||
| 			} | ||||
| 
 | ||||
| 			if part == "from" { | ||||
| 				entry.FromAddress = parts[i+1] | ||||
| 			} else if part == "accepted" { | ||||
| 				entry.ToAddress = parts[i+1] | ||||
| 			} else if strings.HasPrefix(part, "[") { | ||||
| 				entry.Inbound = part[1:] | ||||
| 			} else if strings.HasSuffix(part, "]") { | ||||
| 				entry.Outbound = part[:len(part)-1] | ||||
| 			} else if part == "email:" { | ||||
| 				entry.Email = parts[i+1] | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if logEntryContains(line, freedoms) { | ||||
| 			if showDirect == "false" { | ||||
| 				continue | ||||
| 			} | ||||
| 			line = line + " f" | ||||
| 		} else if hasSuffix(line, blackholes) { | ||||
| 			entry.Event = Direct | ||||
| 		} else if logEntryContains(line, blackholes) { | ||||
| 			if showBlocked == "false" { | ||||
| 				continue | ||||
| 			} | ||||
| 			line = line + " b" | ||||
| 			entry.Event = Blocked | ||||
| 		} else { | ||||
| 			if showProxy == "false" { | ||||
| 				continue | ||||
| 			} | ||||
| 			line = line + " p" | ||||
| 			entry.Event = Proxied | ||||
| 		} | ||||
| 
 | ||||
| 		lines = append(lines, line) | ||||
| 		entries = append(entries, entry) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(lines) > countInt { | ||||
| 		lines = lines[len(lines)-countInt:] | ||||
| 	if len(entries) > countInt { | ||||
| 		entries = entries[len(entries)-countInt:] | ||||
| 	} | ||||
| 
 | ||||
| 	return lines | ||||
| 	return entries | ||||
| } | ||||
| 
 | ||||
| func hasSuffix(line string, suffixes []string) bool { | ||||
| func logEntryContains(line string, suffixes []string) bool { | ||||
| 	for _, sfx := range suffixes { | ||||
| 		if strings.HasSuffix(line, sfx+"]") { | ||||
| 		if strings.Contains(line, sfx+"]") { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue