server.go 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781
  1. package service
  2. import (
  3. "archive/zip"
  4. "bufio"
  5. "bytes"
  6. "encoding/json"
  7. "fmt"
  8. "io"
  9. "mime/multipart"
  10. "net/http"
  11. "net/url"
  12. "os"
  13. "os/exec"
  14. "path/filepath"
  15. "regexp"
  16. "runtime"
  17. "slices"
  18. "strconv"
  19. "strings"
  20. "sync"
  21. "time"
  22. "github.com/mhsanaei/3x-ui/v3/config"
  23. "github.com/mhsanaei/3x-ui/v3/database"
  24. "github.com/mhsanaei/3x-ui/v3/logger"
  25. "github.com/mhsanaei/3x-ui/v3/util/common"
  26. "github.com/mhsanaei/3x-ui/v3/util/sys"
  27. "github.com/mhsanaei/3x-ui/v3/xray"
  28. "github.com/google/uuid"
  29. "github.com/shirou/gopsutil/v4/cpu"
  30. "github.com/shirou/gopsutil/v4/disk"
  31. "github.com/shirou/gopsutil/v4/host"
  32. "github.com/shirou/gopsutil/v4/load"
  33. "github.com/shirou/gopsutil/v4/mem"
  34. "github.com/shirou/gopsutil/v4/net"
  35. )
  36. // ProcessState represents the current state of a system process.
  37. type ProcessState string
  38. // Process state constants
  39. const (
  40. Running ProcessState = "running" // Process is running normally
  41. Stop ProcessState = "stop" // Process is stopped
  42. Error ProcessState = "error" // Process is in error state
  43. )
  44. // Status represents comprehensive system and application status information.
  45. // It includes CPU, memory, disk, network statistics, and Xray process status.
  46. type Status struct {
  47. T time.Time `json:"-"`
  48. Cpu float64 `json:"cpu"`
  49. CpuCores int `json:"cpuCores"`
  50. LogicalPro int `json:"logicalPro"`
  51. CpuSpeedMhz float64 `json:"cpuSpeedMhz"`
  52. Mem struct {
  53. Current uint64 `json:"current"`
  54. Total uint64 `json:"total"`
  55. } `json:"mem"`
  56. Swap struct {
  57. Current uint64 `json:"current"`
  58. Total uint64 `json:"total"`
  59. } `json:"swap"`
  60. Disk struct {
  61. Current uint64 `json:"current"`
  62. Total uint64 `json:"total"`
  63. } `json:"disk"`
  64. DiskIO struct {
  65. Read uint64 `json:"read"`
  66. Write uint64 `json:"write"`
  67. } `json:"diskIO"`
  68. DiskTraffic struct {
  69. Read uint64 `json:"read"`
  70. Write uint64 `json:"write"`
  71. } `json:"diskTraffic"`
  72. Xray struct {
  73. State ProcessState `json:"state"`
  74. ErrorMsg string `json:"errorMsg"`
  75. Version string `json:"version"`
  76. } `json:"xray"`
  77. PanelVersion string `json:"panelVersion"`
  78. PanelGuid string `json:"panelGuid"`
  79. Uptime uint64 `json:"uptime"`
  80. Loads []float64 `json:"loads"`
  81. TcpCount int `json:"tcpCount"`
  82. UdpCount int `json:"udpCount"`
  83. NetIO struct {
  84. Up uint64 `json:"up"`
  85. Down uint64 `json:"down"`
  86. PktUp uint64 `json:"pktUp"`
  87. PktDown uint64 `json:"pktDown"`
  88. } `json:"netIO"`
  89. NetTraffic struct {
  90. Sent uint64 `json:"sent"`
  91. Recv uint64 `json:"recv"`
  92. PktSent uint64 `json:"pktSent"`
  93. PktRecv uint64 `json:"pktRecv"`
  94. } `json:"netTraffic"`
  95. PublicIP struct {
  96. IPv4 string `json:"ipv4"`
  97. IPv6 string `json:"ipv6"`
  98. } `json:"publicIP"`
  99. AppStats struct {
  100. Threads uint32 `json:"threads"`
  101. Mem uint64 `json:"mem"`
  102. Uptime uint64 `json:"uptime"`
  103. } `json:"appStats"`
  104. }
  105. // Release represents information about a software release from GitHub.
  106. type Release struct {
  107. TagName string `json:"tag_name"` // The tag name of the release
  108. }
  109. // ServerService provides business logic for server monitoring and management.
  110. // It handles system status collection, IP detection, and application statistics.
  111. type ServerService struct {
  112. xrayService XrayService
  113. inboundService InboundService
  114. settingService SettingService
  115. cachedIPv4 string
  116. cachedIPv6 string
  117. noIPv6 bool
  118. mu sync.Mutex
  119. lastCPUTimes cpu.TimesStat
  120. hasLastCPUSample bool
  121. hasNativeCPUSample bool
  122. emaCPU float64
  123. cachedCpuSpeedMhz float64
  124. lastCpuInfoAttempt time.Time
  125. lastStatusMu sync.RWMutex
  126. lastStatus *Status
  127. versionsCacheMu sync.Mutex
  128. versionsCache *cachedXrayVersions
  129. }
  130. type cachedXrayVersions struct {
  131. versions []string
  132. fetchedAt time.Time
  133. }
  134. // xrayVersionsCacheTTL bounds how often /getXrayVersion hits GitHub. The list
  135. // is purely informational (rendered in the "switch Xray version" picker) so a
  136. // quarter-hour staleness window is fine and saves the API budget.
  137. const xrayVersionsCacheTTL = 15 * time.Minute
  138. // allowedHistoryBuckets is the bucket-second whitelist for time-series
  139. // aggregation endpoints (server + node metrics). Restricting it prevents
  140. // callers from triggering arbitrary aggregation work and keeps the
  141. // frontend's bucket selector self-documenting.
  142. var allowedHistoryBuckets = map[int]bool{
  143. 2: true, // Real-time view
  144. 30: true, // 30s intervals
  145. 60: true, // 1m intervals
  146. 120: true, // 2m intervals
  147. 180: true, // 3m intervals
  148. 300: true, // 5m intervals
  149. }
  150. // IsAllowedHistoryBucket reports whether a bucket-seconds value is in the
  151. // whitelist used by /server/history, /server/cpuHistory, /server/xrayMetricsHistory,
  152. // /server/xrayObservatoryHistory, and /nodes/history.
  153. func IsAllowedHistoryBucket(bucketSeconds int) bool {
  154. return allowedHistoryBuckets[bucketSeconds]
  155. }
  156. // LastStatus returns the most recent Status snapshot collected by
  157. // RefreshStatus. Safe for concurrent readers.
  158. func (s *ServerService) LastStatus() *Status {
  159. s.lastStatusMu.RLock()
  160. defer s.lastStatusMu.RUnlock()
  161. return s.lastStatus
  162. }
  163. // RefreshStatus collects a new system snapshot, stores it as LastStatus, and
  164. // appends it to the system-metrics time series. Returns the new snapshot (may
  165. // be nil if collection failed). Called by the background ticker; the caller is
  166. // responsible for any side effects (websocket broadcast, xray metrics sample).
  167. func (s *ServerService) RefreshStatus() *Status {
  168. next := s.GetStatus(s.LastStatus())
  169. if next == nil {
  170. return nil
  171. }
  172. s.lastStatusMu.Lock()
  173. s.lastStatus = next
  174. s.lastStatusMu.Unlock()
  175. s.AppendStatusSample(time.Now(), next)
  176. return next
  177. }
  178. // GetXrayVersionsCached wraps GetXrayVersions with a TTL cache. On fetch
  179. // failure we serve the last successful list (if any) so the UI doesn't go
  180. // blank during a GitHub API hiccup; if there's no cache at all the underlying
  181. // error is surfaced.
  182. func (s *ServerService) GetXrayVersionsCached() ([]string, error) {
  183. s.versionsCacheMu.Lock()
  184. cache := s.versionsCache
  185. s.versionsCacheMu.Unlock()
  186. if cache != nil && time.Since(cache.fetchedAt) <= xrayVersionsCacheTTL {
  187. return cache.versions, nil
  188. }
  189. versions, err := s.GetXrayVersions()
  190. if err != nil {
  191. if cache != nil {
  192. logger.Warning("GetXrayVersionsCached: serving stale list:", err)
  193. return cache.versions, nil
  194. }
  195. return nil, err
  196. }
  197. s.versionsCacheMu.Lock()
  198. s.versionsCache = &cachedXrayVersions{versions: versions, fetchedAt: time.Now()}
  199. s.versionsCacheMu.Unlock()
  200. return versions, nil
  201. }
  202. // GetDefaultLogOutboundTags scans the default Xray config for freedom and
  203. // blackhole outbound tags so /getXrayLogs can colour-code log lines without
  204. // the controller re-doing the JSON walk. Falls back to the historical
  205. // "direct"/"blocked" defaults when the config can't be read.
  206. func (s *ServerService) GetDefaultLogOutboundTags() (freedoms, blackholes []string) {
  207. config, err := s.settingService.GetDefaultXrayConfig()
  208. if err == nil && config != nil {
  209. if cfgMap, ok := config.(map[string]any); ok {
  210. if outbounds, ok := cfgMap["outbounds"].([]any); ok {
  211. for _, outbound := range outbounds {
  212. obMap, ok := outbound.(map[string]any)
  213. if !ok {
  214. continue
  215. }
  216. tag, _ := obMap["tag"].(string)
  217. if tag == "" {
  218. continue
  219. }
  220. switch obMap["protocol"] {
  221. case "freedom":
  222. freedoms = append(freedoms, tag)
  223. case "blackhole":
  224. blackholes = append(blackholes, tag)
  225. }
  226. }
  227. }
  228. }
  229. }
  230. if len(freedoms) == 0 {
  231. freedoms = []string{"direct"}
  232. }
  233. if len(blackholes) == 0 {
  234. blackholes = []string{"blocked"}
  235. }
  236. return freedoms, blackholes
  237. }
  238. // AggregateCpuHistory returns up to maxPoints averaged buckets of size bucketSeconds.
  239. // Kept for back-compat with the original /panel/api/server/cpuHistory/:bucket route;
  240. // the response key is "cpu" (not "v") so legacy consumers parse unchanged.
  241. func (s *ServerService) AggregateCpuHistory(bucketSeconds int, maxPoints int) []map[string]any {
  242. out := systemMetrics.aggregate("cpu", bucketSeconds, maxPoints)
  243. for _, p := range out {
  244. p["cpu"] = p["v"]
  245. delete(p, "v")
  246. }
  247. return out
  248. }
  249. // AggregateSystemMetric returns up to maxPoints averaged buckets for any
  250. // known system metric (see SystemMetricKeys). Output points have keys
  251. // {"t": unixSec, "v": value}; the caller decides how to format the value.
  252. func (s *ServerService) AggregateSystemMetric(metric string, bucketSeconds int, maxPoints int) []map[string]any {
  253. return systemMetrics.aggregate(metric, bucketSeconds, maxPoints)
  254. }
  255. type LogEntry struct {
  256. DateTime time.Time
  257. FromAddress string
  258. ToAddress string
  259. Inbound string
  260. Outbound string
  261. Email string
  262. Event int
  263. }
  264. func getPublicIP(url string) string {
  265. client := &http.Client{
  266. Timeout: 3 * time.Second,
  267. }
  268. resp, err := client.Get(url)
  269. if err != nil {
  270. return "N/A"
  271. }
  272. defer resp.Body.Close()
  273. // Don't retry if access is blocked or region-restricted
  274. if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnavailableForLegalReasons {
  275. return "N/A"
  276. }
  277. if resp.StatusCode != http.StatusOK {
  278. return "N/A"
  279. }
  280. ip, err := io.ReadAll(resp.Body)
  281. if err != nil {
  282. return "N/A"
  283. }
  284. ipString := strings.TrimSpace(string(ip))
  285. if ipString == "" {
  286. return "N/A"
  287. }
  288. return ipString
  289. }
  290. func (s *ServerService) GetStatus(lastStatus *Status) *Status {
  291. now := time.Now()
  292. status := &Status{
  293. T: now,
  294. }
  295. // CPU stats
  296. util, err := s.sampleCPUUtilization()
  297. if err != nil {
  298. logger.Warning("get cpu percent failed:", err)
  299. } else {
  300. status.Cpu = util
  301. }
  302. status.CpuCores, err = cpu.Counts(false)
  303. if err != nil {
  304. logger.Warning("get cpu cores count failed:", err)
  305. }
  306. status.LogicalPro = runtime.NumCPU()
  307. if status.CpuSpeedMhz = s.cachedCpuSpeedMhz; s.cachedCpuSpeedMhz == 0 && time.Since(s.lastCpuInfoAttempt) > 5*time.Minute {
  308. s.lastCpuInfoAttempt = time.Now()
  309. done := make(chan struct{})
  310. go func() {
  311. defer close(done)
  312. cpuInfos, err := cpu.Info()
  313. if err != nil {
  314. logger.Warning("get cpu info failed:", err)
  315. return
  316. }
  317. if len(cpuInfos) > 0 {
  318. s.cachedCpuSpeedMhz = cpuInfos[0].Mhz
  319. status.CpuSpeedMhz = s.cachedCpuSpeedMhz
  320. } else {
  321. logger.Warning("could not find cpu info")
  322. }
  323. }()
  324. select {
  325. case <-done:
  326. case <-time.After(1500 * time.Millisecond):
  327. logger.Warning("cpu info query timed out; will retry later")
  328. }
  329. } else if s.cachedCpuSpeedMhz != 0 {
  330. status.CpuSpeedMhz = s.cachedCpuSpeedMhz
  331. }
  332. // Uptime
  333. upTime, err := host.Uptime()
  334. if err != nil {
  335. logger.Warning("get uptime failed:", err)
  336. } else {
  337. status.Uptime = upTime
  338. }
  339. // Memory stats
  340. memInfo, err := mem.VirtualMemory()
  341. if err != nil {
  342. logger.Warning("get virtual memory failed:", err)
  343. } else {
  344. status.Mem.Current = memInfo.Used
  345. status.Mem.Total = memInfo.Total
  346. }
  347. swapInfo, err := mem.SwapMemory()
  348. if err != nil {
  349. logger.Warning("get swap memory failed:", err)
  350. } else {
  351. status.Swap.Current = swapInfo.Used
  352. status.Swap.Total = swapInfo.Total
  353. }
  354. // Disk stats
  355. diskInfo, err := disk.Usage("/")
  356. if err != nil {
  357. logger.Warning("get disk usage failed:", err)
  358. } else {
  359. status.Disk.Current = diskInfo.Used
  360. status.Disk.Total = diskInfo.Total
  361. }
  362. diskIOStats, err := disk.IOCounters()
  363. if err != nil {
  364. logger.Warning("get disk io counters failed:", err)
  365. } else {
  366. var totalRead, totalWrite uint64
  367. for _, counter := range diskIOStats {
  368. totalRead += counter.ReadBytes
  369. totalWrite += counter.WriteBytes
  370. }
  371. status.DiskTraffic.Read = totalRead
  372. status.DiskTraffic.Write = totalWrite
  373. if lastStatus != nil {
  374. duration := now.Sub(lastStatus.T)
  375. seconds := float64(duration) / float64(time.Second)
  376. if seconds > 0 && status.DiskTraffic.Read >= lastStatus.DiskTraffic.Read {
  377. status.DiskIO.Read = uint64(float64(status.DiskTraffic.Read-lastStatus.DiskTraffic.Read) / seconds)
  378. }
  379. if seconds > 0 && status.DiskTraffic.Write >= lastStatus.DiskTraffic.Write {
  380. status.DiskIO.Write = uint64(float64(status.DiskTraffic.Write-lastStatus.DiskTraffic.Write) / seconds)
  381. }
  382. }
  383. }
  384. // Load averages
  385. avgState, err := load.Avg()
  386. if err != nil {
  387. logger.Warning("get load avg failed:", err)
  388. } else {
  389. status.Loads = []float64{avgState.Load1, avgState.Load5, avgState.Load15}
  390. }
  391. // Network stats
  392. ioStats, err := net.IOCounters(true)
  393. if err != nil {
  394. logger.Warning("get io counters failed:", err)
  395. } else {
  396. var totalSent, totalRecv, totalPktSent, totalPktRecv uint64
  397. for _, iface := range ioStats {
  398. name := strings.ToLower(iface.Name)
  399. if isVirtualInterface(name) {
  400. continue
  401. }
  402. totalSent += iface.BytesSent
  403. totalRecv += iface.BytesRecv
  404. totalPktSent += iface.PacketsSent
  405. totalPktRecv += iface.PacketsRecv
  406. }
  407. status.NetTraffic.Sent = totalSent
  408. status.NetTraffic.Recv = totalRecv
  409. status.NetTraffic.PktSent = totalPktSent
  410. status.NetTraffic.PktRecv = totalPktRecv
  411. if lastStatus != nil {
  412. duration := now.Sub(lastStatus.T)
  413. seconds := float64(duration) / float64(time.Second)
  414. up := uint64(float64(status.NetTraffic.Sent-lastStatus.NetTraffic.Sent) / seconds)
  415. down := uint64(float64(status.NetTraffic.Recv-lastStatus.NetTraffic.Recv) / seconds)
  416. status.NetIO.Up = up
  417. status.NetIO.Down = down
  418. if seconds > 0 && status.NetTraffic.PktSent >= lastStatus.NetTraffic.PktSent {
  419. status.NetIO.PktUp = uint64(float64(status.NetTraffic.PktSent-lastStatus.NetTraffic.PktSent) / seconds)
  420. }
  421. if seconds > 0 && status.NetTraffic.PktRecv >= lastStatus.NetTraffic.PktRecv {
  422. status.NetIO.PktDown = uint64(float64(status.NetTraffic.PktRecv-lastStatus.NetTraffic.PktRecv) / seconds)
  423. }
  424. }
  425. }
  426. // TCP/UDP connections
  427. status.TcpCount, err = sys.GetTCPCount()
  428. if err != nil {
  429. logger.Warning("get tcp connections failed:", err)
  430. }
  431. status.UdpCount, err = sys.GetUDPCount()
  432. if err != nil {
  433. logger.Warning("get udp connections failed:", err)
  434. }
  435. // IP fetching with caching
  436. showIp4ServiceLists := []string{
  437. "https://api4.ipify.org",
  438. "https://ipv4.icanhazip.com",
  439. "https://v4.api.ipinfo.io/ip",
  440. "https://ipv4.myexternalip.com/raw",
  441. "https://4.ident.me",
  442. "https://check-host.net/ip",
  443. }
  444. showIp6ServiceLists := []string{
  445. "https://api6.ipify.org",
  446. "https://ipv6.icanhazip.com",
  447. "https://v6.api.ipinfo.io/ip",
  448. "https://ipv6.myexternalip.com/raw",
  449. "https://6.ident.me",
  450. }
  451. if s.cachedIPv4 == "" {
  452. for _, ip4Service := range showIp4ServiceLists {
  453. s.cachedIPv4 = getPublicIP(ip4Service)
  454. if s.cachedIPv4 != "N/A" {
  455. break
  456. }
  457. }
  458. }
  459. if s.cachedIPv6 == "" && !s.noIPv6 {
  460. for _, ip6Service := range showIp6ServiceLists {
  461. s.cachedIPv6 = getPublicIP(ip6Service)
  462. if s.cachedIPv6 != "N/A" {
  463. break
  464. }
  465. }
  466. }
  467. if s.cachedIPv6 == "N/A" {
  468. s.noIPv6 = true
  469. }
  470. status.PublicIP.IPv4 = s.cachedIPv4
  471. status.PublicIP.IPv6 = s.cachedIPv6
  472. // Xray status
  473. if s.xrayService.IsXrayRunning() {
  474. status.Xray.State = Running
  475. status.Xray.ErrorMsg = ""
  476. } else {
  477. err := s.xrayService.GetXrayErr()
  478. if err != nil {
  479. status.Xray.State = Error
  480. } else {
  481. status.Xray.State = Stop
  482. }
  483. status.Xray.ErrorMsg = s.xrayService.GetXrayResult()
  484. }
  485. status.Xray.Version = s.xrayService.GetXrayVersion()
  486. status.PanelVersion = config.GetVersion()
  487. if guid, err := s.settingService.GetPanelGuid(); err == nil {
  488. status.PanelGuid = guid
  489. }
  490. // Application stats
  491. var rtm runtime.MemStats
  492. runtime.ReadMemStats(&rtm)
  493. status.AppStats.Mem = rtm.Sys
  494. status.AppStats.Threads = uint32(runtime.NumGoroutine())
  495. if p != nil && p.IsRunning() {
  496. status.AppStats.Uptime = p.GetUptime()
  497. } else {
  498. status.AppStats.Uptime = 0
  499. }
  500. return status
  501. }
  502. // AppendCpuSample is preserved for callers that only have the CPU number.
  503. // New callers should prefer AppendStatusSample which writes the full set.
  504. func (s *ServerService) AppendCpuSample(t time.Time, v float64) {
  505. systemMetrics.append("cpu", t, v)
  506. }
  507. // AppendStatusSample writes one tick of every metric we keep — CPU, memory
  508. // percent, network throughput (bytes/s), online client count, and the three
  509. // load averages. Called by RefreshStatus on the same @2s cadence as
  510. // AppendCpuSample, so all series stay aligned.
  511. func (s *ServerService) AppendStatusSample(t time.Time, status *Status) {
  512. if status == nil {
  513. return
  514. }
  515. systemMetrics.append("cpu", t, status.Cpu)
  516. if status.Mem.Total > 0 {
  517. systemMetrics.append("mem", t, float64(status.Mem.Current)*100.0/float64(status.Mem.Total))
  518. }
  519. if status.Swap.Total > 0 {
  520. systemMetrics.append("swap", t, float64(status.Swap.Current)*100.0/float64(status.Swap.Total))
  521. } else {
  522. systemMetrics.append("swap", t, 0)
  523. }
  524. systemMetrics.append("netUp", t, float64(status.NetIO.Up))
  525. systemMetrics.append("netDown", t, float64(status.NetIO.Down))
  526. systemMetrics.append("diskRead", t, float64(status.DiskIO.Read))
  527. systemMetrics.append("diskWrite", t, float64(status.DiskIO.Write))
  528. if status.Disk.Total > 0 {
  529. systemMetrics.append("diskUsage", t, float64(status.Disk.Current)*100.0/float64(status.Disk.Total))
  530. }
  531. systemMetrics.append("pktUp", t, float64(status.NetIO.PktUp))
  532. systemMetrics.append("pktDown", t, float64(status.NetIO.PktDown))
  533. systemMetrics.append("tcpCount", t, float64(status.TcpCount))
  534. systemMetrics.append("udpCount", t, float64(status.UdpCount))
  535. online := 0
  536. if p != nil && p.IsRunning() {
  537. online = len(p.GetOnlineClients())
  538. }
  539. systemMetrics.append("online", t, float64(online))
  540. if len(status.Loads) >= 3 {
  541. systemMetrics.append("load1", t, status.Loads[0])
  542. systemMetrics.append("load5", t, status.Loads[1])
  543. systemMetrics.append("load15", t, status.Loads[2])
  544. }
  545. }
  546. func (s *ServerService) sampleCPUUtilization() (float64, error) {
  547. // Try native platform-specific CPU implementation first (Windows, Linux, macOS)
  548. if pct, err := sys.CPUPercentRaw(); err == nil {
  549. s.mu.Lock()
  550. // First call to native method returns 0 (initializes baseline)
  551. if !s.hasNativeCPUSample {
  552. s.hasNativeCPUSample = true
  553. s.mu.Unlock()
  554. return 0, nil
  555. }
  556. // Smooth with EMA
  557. const alpha = 0.3
  558. if s.emaCPU == 0 {
  559. s.emaCPU = pct
  560. } else {
  561. s.emaCPU = alpha*pct + (1-alpha)*s.emaCPU
  562. }
  563. val := s.emaCPU
  564. s.mu.Unlock()
  565. return val, nil
  566. }
  567. // If native call fails, fall back to gopsutil times
  568. // Read aggregate CPU times (all CPUs combined)
  569. times, err := cpu.Times(false)
  570. if err != nil {
  571. return 0, err
  572. }
  573. if len(times) == 0 {
  574. return 0, fmt.Errorf("no cpu times available")
  575. }
  576. cur := times[0]
  577. s.mu.Lock()
  578. defer s.mu.Unlock()
  579. // If this is the first sample, initialize and return current EMA (0 by default)
  580. if !s.hasLastCPUSample {
  581. s.lastCPUTimes = cur
  582. s.hasLastCPUSample = true
  583. return s.emaCPU, nil
  584. }
  585. // Compute busy and total deltas
  586. // Note: Guest and GuestNice times are already included in User and Nice respectively,
  587. // so we exclude them to avoid double-counting (Linux kernel accounting)
  588. idleDelta := cur.Idle - s.lastCPUTimes.Idle
  589. busyDelta := (cur.User - s.lastCPUTimes.User) +
  590. (cur.System - s.lastCPUTimes.System) +
  591. (cur.Nice - s.lastCPUTimes.Nice) +
  592. (cur.Iowait - s.lastCPUTimes.Iowait) +
  593. (cur.Irq - s.lastCPUTimes.Irq) +
  594. (cur.Softirq - s.lastCPUTimes.Softirq) +
  595. (cur.Steal - s.lastCPUTimes.Steal)
  596. totalDelta := busyDelta + idleDelta
  597. // Update last sample for next time
  598. s.lastCPUTimes = cur
  599. // Guard against division by zero or negative deltas (e.g., counter resets)
  600. if totalDelta <= 0 {
  601. return s.emaCPU, nil
  602. }
  603. raw := 100.0 * (busyDelta / totalDelta)
  604. if raw < 0 {
  605. raw = 0
  606. }
  607. if raw > 100 {
  608. raw = 100
  609. }
  610. // Exponential moving average to smooth spikes
  611. const alpha = 0.3 // smoothing factor (0<alpha<=1). Higher = more responsive, lower = smoother
  612. if s.emaCPU == 0 {
  613. // Initialize EMA with the first real reading to avoid long warm-up from zero
  614. s.emaCPU = raw
  615. } else {
  616. s.emaCPU = alpha*raw + (1-alpha)*s.emaCPU
  617. }
  618. return s.emaCPU, nil
  619. }
  620. const (
  621. maxXrayArchiveBytes = 200 << 20
  622. maxXrayBinaryBytes = 200 << 20
  623. )
  624. func (s *ServerService) GetXrayVersions() ([]string, error) {
  625. const (
  626. XrayURL = "https://api.github.com/repos/XTLS/Xray-core/releases"
  627. bufferSize = 8192
  628. )
  629. resp, err := s.settingService.NewProxiedHTTPClient(10 * time.Second).Get(XrayURL)
  630. if err != nil {
  631. return nil, err
  632. }
  633. defer resp.Body.Close()
  634. // Check HTTP status code - GitHub API returns object instead of array on error
  635. if resp.StatusCode != http.StatusOK {
  636. bodyBytes, _ := io.ReadAll(resp.Body)
  637. var errorResponse struct {
  638. Message string `json:"message"`
  639. }
  640. if json.Unmarshal(bodyBytes, &errorResponse) == nil && errorResponse.Message != "" {
  641. return nil, fmt.Errorf("GitHub API error: %s", errorResponse.Message)
  642. }
  643. return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, resp.Status)
  644. }
  645. buffer := bytes.NewBuffer(make([]byte, bufferSize))
  646. buffer.Reset()
  647. if _, err := buffer.ReadFrom(resp.Body); err != nil {
  648. return nil, err
  649. }
  650. var releases []Release
  651. if err := json.Unmarshal(buffer.Bytes(), &releases); err != nil {
  652. return nil, err
  653. }
  654. var versions []string
  655. for _, release := range releases {
  656. tagVersion := strings.TrimPrefix(release.TagName, "v")
  657. tagParts := strings.Split(tagVersion, ".")
  658. if len(tagParts) != 3 {
  659. continue
  660. }
  661. major, err1 := strconv.Atoi(tagParts[0])
  662. minor, err2 := strconv.Atoi(tagParts[1])
  663. patch, err3 := strconv.Atoi(tagParts[2])
  664. if err1 != nil || err2 != nil || err3 != nil {
  665. continue
  666. }
  667. if major > 26 || (major == 26 && minor > 4) || (major == 26 && minor == 4 && patch >= 25) {
  668. versions = append(versions, release.TagName)
  669. }
  670. }
  671. return versions, nil
  672. }
  673. func (s *ServerService) StopXrayService() error {
  674. err := s.xrayService.StopXray()
  675. if err != nil {
  676. logger.Error("stop xray failed:", err)
  677. return err
  678. }
  679. return nil
  680. }
  681. func (s *ServerService) RestartXrayService() error {
  682. err := s.xrayService.RestartXray(true)
  683. if err != nil {
  684. logger.Error("start xray failed:", err)
  685. return err
  686. }
  687. return nil
  688. }
  689. func (s *ServerService) downloadXRay(version string) (string, error) {
  690. osName := runtime.GOOS
  691. arch := runtime.GOARCH
  692. switch osName {
  693. case "darwin":
  694. osName = "macos"
  695. case "windows":
  696. osName = "windows"
  697. }
  698. switch arch {
  699. case "amd64":
  700. arch = "64"
  701. case "arm64":
  702. arch = "arm64-v8a"
  703. case "armv7":
  704. arch = "arm32-v7a"
  705. case "armv6":
  706. arch = "arm32-v6"
  707. case "armv5":
  708. arch = "arm32-v5"
  709. case "386":
  710. arch = "32"
  711. case "s390x":
  712. arch = "s390x"
  713. }
  714. fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
  715. url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
  716. client := s.settingService.NewProxiedHTTPClient(60 * time.Second)
  717. resp, err := client.Get(url)
  718. if err != nil {
  719. return "", err
  720. }
  721. defer resp.Body.Close()
  722. if resp.StatusCode != http.StatusOK {
  723. return "", fmt.Errorf("download xray: unexpected HTTP %d", resp.StatusCode)
  724. }
  725. if resp.ContentLength > maxXrayArchiveBytes {
  726. return "", fmt.Errorf("download xray: archive exceeds %d bytes", maxXrayArchiveBytes)
  727. }
  728. file, err := os.CreateTemp("", "xray-*.zip")
  729. if err != nil {
  730. return "", err
  731. }
  732. path := file.Name()
  733. ok := false
  734. defer func() {
  735. _ = file.Close()
  736. if !ok {
  737. _ = os.Remove(path)
  738. }
  739. }()
  740. n, err := io.Copy(file, io.LimitReader(resp.Body, maxXrayArchiveBytes+1))
  741. if err != nil {
  742. return "", err
  743. }
  744. if n > maxXrayArchiveBytes {
  745. return "", fmt.Errorf("download xray: archive exceeds %d bytes", maxXrayArchiveBytes)
  746. }
  747. ok = true
  748. return path, nil
  749. }
  750. func (s *ServerService) UpdateXray(version string) error {
  751. versions, err := s.GetXrayVersions()
  752. if err != nil {
  753. return err
  754. }
  755. if !slices.Contains(versions, version) {
  756. return fmt.Errorf("xray version %q is not in the fetched release list", version)
  757. }
  758. // 1. Stop xray before doing anything
  759. if err := s.StopXrayService(); err != nil {
  760. logger.Warning("failed to stop xray before update:", err)
  761. }
  762. // 2. Download the zip
  763. zipFileName, err := s.downloadXRay(version)
  764. if err != nil {
  765. return err
  766. }
  767. defer os.Remove(zipFileName)
  768. zipFile, err := os.Open(zipFileName)
  769. if err != nil {
  770. return err
  771. }
  772. defer zipFile.Close()
  773. stat, err := zipFile.Stat()
  774. if err != nil {
  775. return err
  776. }
  777. reader, err := zip.NewReader(zipFile, stat.Size())
  778. if err != nil {
  779. return err
  780. }
  781. // 3. Helper to extract files
  782. copyZipFile := func(zipName string, fileName string) error {
  783. zipFile, err := reader.Open(zipName)
  784. if err != nil {
  785. return err
  786. }
  787. defer zipFile.Close()
  788. if err := os.MkdirAll(filepath.Dir(fileName), 0755); err != nil {
  789. return err
  790. }
  791. tmpFile, err := os.CreateTemp(filepath.Dir(fileName), ".xray-*")
  792. if err != nil {
  793. return err
  794. }
  795. tmpPath := tmpFile.Name()
  796. ok := false
  797. defer func() {
  798. _ = tmpFile.Close()
  799. if !ok {
  800. _ = os.Remove(tmpPath)
  801. }
  802. }()
  803. n, err := io.Copy(tmpFile, io.LimitReader(zipFile, maxXrayBinaryBytes+1))
  804. if err != nil {
  805. return err
  806. }
  807. if n > maxXrayBinaryBytes {
  808. return fmt.Errorf("xray binary exceeds %d bytes", maxXrayBinaryBytes)
  809. }
  810. if err := tmpFile.Chmod(0755); err != nil {
  811. return err
  812. }
  813. if err := tmpFile.Close(); err != nil {
  814. return err
  815. }
  816. if runtime.GOOS == "windows" {
  817. _ = os.Remove(fileName)
  818. }
  819. if err := os.Rename(tmpPath, fileName); err != nil {
  820. return err
  821. }
  822. ok = true
  823. return nil
  824. }
  825. // 4. Extract correct binary
  826. if runtime.GOOS == "windows" {
  827. targetBinary := filepath.Join(config.GetBinFolderPath(), "xray-windows-amd64.exe")
  828. err = copyZipFile("xray.exe", targetBinary)
  829. } else {
  830. err = copyZipFile("xray", xray.GetBinaryPath())
  831. }
  832. if err != nil {
  833. return err
  834. }
  835. // 5. Restart xray
  836. if err := s.xrayService.RestartXray(true); err != nil {
  837. logger.Error("start xray failed:", err)
  838. return err
  839. }
  840. return nil
  841. }
  842. func (s *ServerService) GetLogs(count string, level string, syslog string) []string {
  843. c, _ := strconv.Atoi(count)
  844. var lines []string
  845. if syslog == "true" {
  846. // Check if running on Windows - journalctl is not available
  847. if runtime.GOOS == "windows" {
  848. return []string{"Syslog is not supported on Windows. Please use application logs instead by unchecking the 'Syslog' option."}
  849. }
  850. // Validate and sanitize count parameter
  851. countInt, err := strconv.Atoi(count)
  852. if err != nil || countInt < 1 || countInt > 10000 {
  853. return []string{"Invalid count parameter - must be a number between 1 and 10000"}
  854. }
  855. // Validate level parameter - only allow valid syslog levels
  856. validLevels := map[string]bool{
  857. "0": true, "emerg": true,
  858. "1": true, "alert": true,
  859. "2": true, "crit": true,
  860. "3": true, "err": true,
  861. "4": true, "warning": true,
  862. "5": true, "notice": true,
  863. "6": true, "info": true,
  864. "7": true, "debug": true,
  865. }
  866. if !validLevels[level] {
  867. return []string{"Invalid level parameter - must be a valid syslog level"}
  868. }
  869. // Use hardcoded command with validated parameters
  870. cmd := exec.Command("journalctl", "-u", "x-ui", "--no-pager", "-n", strconv.Itoa(countInt), "-p", level)
  871. var out bytes.Buffer
  872. cmd.Stdout = &out
  873. err = cmd.Run()
  874. if err != nil {
  875. return []string{"Failed to run journalctl command! Make sure systemd is available and x-ui service is registered."}
  876. }
  877. lines = strings.Split(out.String(), "\n")
  878. } else {
  879. lines = logger.GetLogs(c, level)
  880. }
  881. return lines
  882. }
  883. func (s *ServerService) GetXrayLogs(
  884. count string,
  885. filter string,
  886. showDirect string,
  887. showBlocked string,
  888. showProxy string,
  889. freedoms []string,
  890. blackholes []string) []LogEntry {
  891. const (
  892. Direct = iota
  893. Blocked
  894. Proxied
  895. )
  896. countInt, _ := strconv.Atoi(count)
  897. var entries []LogEntry
  898. pathToAccessLog, err := xray.GetAccessLogPath()
  899. if err != nil {
  900. return nil
  901. }
  902. file, err := os.Open(pathToAccessLog)
  903. if err != nil {
  904. return nil
  905. }
  906. defer file.Close()
  907. scanner := bufio.NewScanner(file)
  908. for scanner.Scan() {
  909. line := strings.TrimSpace(scanner.Text())
  910. if line == "" || strings.Contains(line, "api -> api") {
  911. //skipping empty lines and api calls
  912. continue
  913. }
  914. if filter != "" && !strings.Contains(line, filter) {
  915. //applying filter if it's not empty
  916. continue
  917. }
  918. var entry LogEntry
  919. parts := strings.Fields(line)
  920. for i, part := range parts {
  921. if i == 0 {
  922. dateTime, err := time.ParseInLocation("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1], time.Local)
  923. if err != nil {
  924. continue
  925. }
  926. entry.DateTime = dateTime.UTC()
  927. }
  928. if part == "from" {
  929. entry.FromAddress = strings.TrimLeft(parts[i+1], "/")
  930. } else if part == "accepted" {
  931. entry.ToAddress = strings.TrimLeft(parts[i+1], "/")
  932. } else if strings.HasPrefix(part, "[") {
  933. entry.Inbound = part[1:]
  934. } else if strings.HasSuffix(part, "]") {
  935. entry.Outbound = part[:len(part)-1]
  936. } else if part == "email:" {
  937. entry.Email = parts[i+1]
  938. }
  939. }
  940. if logEntryContains(line, freedoms) {
  941. if showDirect == "false" {
  942. continue
  943. }
  944. entry.Event = Direct
  945. } else if logEntryContains(line, blackholes) {
  946. if showBlocked == "false" {
  947. continue
  948. }
  949. entry.Event = Blocked
  950. } else {
  951. if showProxy == "false" {
  952. continue
  953. }
  954. entry.Event = Proxied
  955. }
  956. entries = append(entries, entry)
  957. }
  958. if err := scanner.Err(); err != nil {
  959. return nil
  960. }
  961. if len(entries) > countInt {
  962. entries = entries[len(entries)-countInt:]
  963. }
  964. return entries
  965. }
  966. // isVirtualInterface returns true for loopback and virtual/tunnel interfaces
  967. // that should be excluded from network traffic statistics.
  968. func isVirtualInterface(name string) bool {
  969. // Exact matches
  970. if name == "lo" || name == "lo0" {
  971. return true
  972. }
  973. // Prefix matches for virtual/tunnel interfaces
  974. virtualPrefixes := []string{
  975. "loopback",
  976. "docker",
  977. "br-",
  978. "veth",
  979. "virbr",
  980. "tun",
  981. "tap",
  982. "wg",
  983. "tailscale",
  984. "zt",
  985. }
  986. for _, prefix := range virtualPrefixes {
  987. if strings.HasPrefix(name, prefix) {
  988. return true
  989. }
  990. }
  991. return false
  992. }
  993. func logEntryContains(line string, suffixes []string) bool {
  994. for _, sfx := range suffixes {
  995. if strings.Contains(line, sfx+"]") {
  996. return true
  997. }
  998. }
  999. return false
  1000. }
  1001. func (s *ServerService) GetConfigJson() (any, error) {
  1002. config, err := s.xrayService.GetXrayConfig()
  1003. if err != nil {
  1004. return nil, err
  1005. }
  1006. contents, err := json.MarshalIndent(config, "", " ")
  1007. if err != nil {
  1008. return nil, err
  1009. }
  1010. var jsonData any
  1011. err = json.Unmarshal(contents, &jsonData)
  1012. if err != nil {
  1013. return nil, err
  1014. }
  1015. return jsonData, nil
  1016. }
  1017. func (s *ServerService) GetDb() ([]byte, error) {
  1018. if database.IsPostgres() {
  1019. return s.exportPostgresDB()
  1020. }
  1021. // Update by manually trigger a checkpoint operation
  1022. err := database.Checkpoint()
  1023. if err != nil {
  1024. return nil, err
  1025. }
  1026. // Open the file for reading
  1027. file, err := os.Open(config.GetDBPath())
  1028. if err != nil {
  1029. return nil, err
  1030. }
  1031. defer file.Close()
  1032. // Read the file contents
  1033. fileContents, err := io.ReadAll(file)
  1034. if err != nil {
  1035. return nil, err
  1036. }
  1037. return fileContents, nil
  1038. }
  1039. // GetMigration produces a cross-engine migration file plus its filename: on a
  1040. // SQLite panel it returns a portable .dump (SQL text), and on a PostgreSQL panel
  1041. // it returns a .db SQLite database built from the live data. Either output can
  1042. // then seed a panel running on the other backend.
  1043. func (s *ServerService) GetMigration() ([]byte, string, error) {
  1044. if database.IsPostgres() {
  1045. tmp, err := os.CreateTemp("", "x-ui-migration-*.db")
  1046. if err != nil {
  1047. return nil, "", err
  1048. }
  1049. tmpPath := tmp.Name()
  1050. tmp.Close()
  1051. defer os.Remove(tmpPath)
  1052. if err := database.ExportPostgresToSQLite(config.GetDBDSN(), tmpPath); err != nil {
  1053. return nil, "", err
  1054. }
  1055. data, err := os.ReadFile(tmpPath)
  1056. if err != nil {
  1057. return nil, "", err
  1058. }
  1059. return data, "x-ui.db", nil
  1060. }
  1061. // SQLite panel: checkpoint so the .db reflects the latest writes, then dump.
  1062. if err := database.Checkpoint(); err != nil {
  1063. return nil, "", err
  1064. }
  1065. data, err := database.DumpSQLiteToBytes(config.GetDBPath())
  1066. if err != nil {
  1067. return nil, "", err
  1068. }
  1069. return data, "x-ui.dump", nil
  1070. }
  1071. func (s *ServerService) ImportDB(file multipart.File) error {
  1072. if database.IsPostgres() {
  1073. return s.importPostgresDB(file)
  1074. }
  1075. // Check if the file is a SQLite database
  1076. isValidDb, err := database.IsSQLiteDB(file)
  1077. if err != nil {
  1078. return common.NewErrorf("Error checking db file format: %v", err)
  1079. }
  1080. if !isValidDb {
  1081. return common.NewError("Invalid db file format")
  1082. }
  1083. // Reset the file reader to the beginning
  1084. _, err = file.Seek(0, 0)
  1085. if err != nil {
  1086. return common.NewErrorf("Error resetting file reader: %v", err)
  1087. }
  1088. // Save the file as a temporary file
  1089. tempPath := fmt.Sprintf("%s.temp", config.GetDBPath())
  1090. // Remove the existing temporary file (if any)
  1091. if _, err := os.Stat(tempPath); err == nil {
  1092. if errRemove := os.Remove(tempPath); errRemove != nil {
  1093. return common.NewErrorf("Error removing existing temporary db file: %v", errRemove)
  1094. }
  1095. }
  1096. // Create the temporary file
  1097. tempFile, err := os.Create(tempPath)
  1098. if err != nil {
  1099. return common.NewErrorf("Error creating temporary db file: %v", err)
  1100. }
  1101. // Robust deferred cleanup for the temporary file
  1102. defer func() {
  1103. if tempFile != nil {
  1104. if cerr := tempFile.Close(); cerr != nil {
  1105. logger.Warningf("Warning: failed to close temp file: %v", cerr)
  1106. }
  1107. }
  1108. if _, err := os.Stat(tempPath); err == nil {
  1109. if rerr := os.Remove(tempPath); rerr != nil {
  1110. logger.Warningf("Warning: failed to remove temp file: %v", rerr)
  1111. }
  1112. }
  1113. }()
  1114. // Save uploaded file to temporary file
  1115. if _, err = io.Copy(tempFile, file); err != nil {
  1116. return common.NewErrorf("Error saving db: %v", err)
  1117. }
  1118. // Close temp file before opening via sqlite
  1119. if err = tempFile.Close(); err != nil {
  1120. return common.NewErrorf("Error closing temporary db file: %v", err)
  1121. }
  1122. tempFile = nil
  1123. // Validate integrity (no migrations / side effects)
  1124. if err = database.ValidateSQLiteDB(tempPath); err != nil {
  1125. return common.NewErrorf("Invalid or corrupt db file: %v", err)
  1126. }
  1127. xrayStopped := true
  1128. defer func() {
  1129. if xrayStopped {
  1130. if errR := s.RestartXrayService(); errR != nil {
  1131. logger.Warningf("Failed to restart Xray after DB import error: %v", errR)
  1132. }
  1133. }
  1134. }()
  1135. if errStop := s.StopXrayService(); errStop != nil {
  1136. logger.Warningf("Failed to stop Xray before DB import: %v", errStop)
  1137. }
  1138. if errClose := database.CloseDB(); errClose != nil {
  1139. logger.Warningf("Failed to close existing DB before replacement: %v", errClose)
  1140. }
  1141. // Backup the current database for fallback
  1142. fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
  1143. // Remove the existing fallback file (if any)
  1144. if _, err := os.Stat(fallbackPath); err == nil {
  1145. if errRemove := os.Remove(fallbackPath); errRemove != nil {
  1146. return common.NewErrorf("Error removing existing fallback db file: %v", errRemove)
  1147. }
  1148. }
  1149. // Move the current database to the fallback location
  1150. if err = os.Rename(config.GetDBPath(), fallbackPath); err != nil {
  1151. return common.NewErrorf("Error backing up current db file: %v", err)
  1152. }
  1153. // Defer fallback cleanup ONLY if everything goes well
  1154. defer func() {
  1155. if _, err := os.Stat(fallbackPath); err == nil {
  1156. if rerr := os.Remove(fallbackPath); rerr != nil {
  1157. logger.Warningf("Warning: failed to remove fallback file: %v", rerr)
  1158. }
  1159. }
  1160. }()
  1161. // Move temp to DB path
  1162. if err = os.Rename(tempPath, config.GetDBPath()); err != nil {
  1163. // Restore from fallback
  1164. if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil {
  1165. return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename)
  1166. }
  1167. return common.NewErrorf("Error moving db file: %v", err)
  1168. }
  1169. // Open & migrate new DB
  1170. if err = database.InitDB(config.GetDBPath()); err != nil {
  1171. if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil {
  1172. return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
  1173. }
  1174. return common.NewErrorf("Error migrating db: %v", err)
  1175. }
  1176. s.inboundService.MigrateDB()
  1177. xrayStopped = false
  1178. if err = s.RestartXrayService(); err != nil {
  1179. return common.NewErrorf("Imported DB but failed to start Xray: %v", err)
  1180. }
  1181. return nil
  1182. }
  1183. // pgConnEnv turns the configured PostgreSQL DSN into the PG* environment used by
  1184. // pg_dump/pg_restore, keeping the password out of the process argument list.
  1185. func pgConnEnv(dsn string) (env []string, dbname string, err error) {
  1186. u, err := url.Parse(strings.TrimSpace(dsn))
  1187. if err != nil {
  1188. return nil, "", err
  1189. }
  1190. if u.Scheme != "postgres" && u.Scheme != "postgresql" {
  1191. return nil, "", common.NewErrorf("unsupported DSN scheme %q", u.Scheme)
  1192. }
  1193. dbname = strings.TrimPrefix(u.Path, "/")
  1194. if dbname == "" {
  1195. return nil, "", common.NewError("PostgreSQL DSN is missing a database name")
  1196. }
  1197. host := u.Hostname()
  1198. if host == "" {
  1199. host = "127.0.0.1"
  1200. }
  1201. port := u.Port()
  1202. if port == "" {
  1203. port = "5432"
  1204. }
  1205. env = append(os.Environ(), "PGHOST="+host, "PGPORT="+port, "PGDATABASE="+dbname)
  1206. if user := u.User.Username(); user != "" {
  1207. env = append(env, "PGUSER="+user)
  1208. }
  1209. if pass, ok := u.User.Password(); ok {
  1210. env = append(env, "PGPASSWORD="+pass)
  1211. }
  1212. if sslmode := u.Query().Get("sslmode"); sslmode != "" {
  1213. env = append(env, "PGSSLMODE="+sslmode)
  1214. }
  1215. return env, dbname, nil
  1216. }
  1217. func (s *ServerService) exportPostgresDB() ([]byte, error) {
  1218. bin, err := exec.LookPath("pg_dump")
  1219. if err != nil {
  1220. return nil, common.NewError("pg_dump not found on the server; install the postgresql-client package to back up a PostgreSQL database")
  1221. }
  1222. env, dbname, err := pgConnEnv(config.GetDBDSN())
  1223. if err != nil {
  1224. return nil, common.NewErrorf("invalid PostgreSQL DSN: %v", err)
  1225. }
  1226. cmd := exec.Command(bin, "--format=custom", "--no-owner", "--no-privileges", "--dbname", dbname)
  1227. cmd.Env = env
  1228. var out, stderr bytes.Buffer
  1229. cmd.Stdout = &out
  1230. cmd.Stderr = &stderr
  1231. if err := cmd.Run(); err != nil {
  1232. return nil, common.NewErrorf("pg_dump failed: %v: %s", err, strings.TrimSpace(stderr.String()))
  1233. }
  1234. return out.Bytes(), nil
  1235. }
  1236. func (s *ServerService) importPostgresDB(file multipart.File) error {
  1237. header := make([]byte, 5)
  1238. if _, err := file.ReadAt(header, 0); err != nil {
  1239. return common.NewErrorf("Error reading dump file: %v", err)
  1240. }
  1241. if string(header) != "PGDMP" {
  1242. return common.NewError("Invalid file: expected a PostgreSQL custom-format dump (.dump) created by this panel's Back Up")
  1243. }
  1244. if _, err := file.Seek(0, 0); err != nil {
  1245. return common.NewErrorf("Error resetting file reader: %v", err)
  1246. }
  1247. bin, err := exec.LookPath("pg_restore")
  1248. if err != nil {
  1249. return common.NewError("pg_restore not found on the server; install the postgresql-client package to restore a PostgreSQL database")
  1250. }
  1251. env, dbname, err := pgConnEnv(config.GetDBDSN())
  1252. if err != nil {
  1253. return common.NewErrorf("invalid PostgreSQL DSN: %v", err)
  1254. }
  1255. tempFile, err := os.CreateTemp("", "x-ui-pg-restore-*.dump")
  1256. if err != nil {
  1257. return common.NewErrorf("Error creating temporary dump file: %v", err)
  1258. }
  1259. tempPath := tempFile.Name()
  1260. defer os.Remove(tempPath)
  1261. if _, err := io.Copy(tempFile, file); err != nil {
  1262. tempFile.Close()
  1263. return common.NewErrorf("Error saving dump: %v", err)
  1264. }
  1265. if err := tempFile.Close(); err != nil {
  1266. return common.NewErrorf("Error closing temporary dump file: %v", err)
  1267. }
  1268. xrayStopped := true
  1269. defer func() {
  1270. if xrayStopped {
  1271. if errR := s.RestartXrayService(); errR != nil {
  1272. logger.Warningf("Failed to restart Xray after DB restore error: %v", errR)
  1273. }
  1274. }
  1275. }()
  1276. if errStop := s.StopXrayService(); errStop != nil {
  1277. logger.Warningf("Failed to stop Xray before DB restore: %v", errStop)
  1278. }
  1279. if errClose := database.CloseDB(); errClose != nil {
  1280. logger.Warningf("Failed to close existing DB before restore: %v", errClose)
  1281. }
  1282. cmd := exec.Command(bin,
  1283. "--clean", "--if-exists", "--no-owner", "--no-privileges",
  1284. "--single-transaction", "--dbname", dbname, tempPath,
  1285. )
  1286. cmd.Env = env
  1287. var stderr bytes.Buffer
  1288. cmd.Stderr = &stderr
  1289. runErr := cmd.Run()
  1290. if errInit := database.InitDB(config.GetDBPath()); errInit != nil {
  1291. return common.NewErrorf("Restore finished but reopening the database failed: %v", errInit)
  1292. }
  1293. s.inboundService.MigrateDB()
  1294. if runErr != nil {
  1295. return common.NewErrorf("pg_restore failed (database left unchanged): %v: %s", runErr, strings.TrimSpace(stderr.String()))
  1296. }
  1297. xrayStopped = false
  1298. if err := s.RestartXrayService(); err != nil {
  1299. return common.NewErrorf("Restored DB but failed to start Xray: %v", err)
  1300. }
  1301. return nil
  1302. }
  1303. // IsValidGeofileName validates that the filename is safe for geofile operations.
  1304. // It checks for path traversal attempts and ensures the filename contains only safe characters.
  1305. func (s *ServerService) IsValidGeofileName(filename string) bool {
  1306. if filename == "" {
  1307. return false
  1308. }
  1309. // Check for path traversal attempts
  1310. if strings.Contains(filename, "..") {
  1311. return false
  1312. }
  1313. // Check for path separators (both forward and backward slash)
  1314. if strings.ContainsAny(filename, `/\`) {
  1315. return false
  1316. }
  1317. // Check for absolute path indicators
  1318. if filepath.IsAbs(filename) {
  1319. return false
  1320. }
  1321. // Additional security: only allow alphanumeric, dots, underscores, and hyphens
  1322. // This is stricter than the general filename regex
  1323. validGeofilePattern := `^[a-zA-Z0-9._-]+\.dat$`
  1324. matched, _ := regexp.MatchString(validGeofilePattern, filename)
  1325. return matched
  1326. }
  1327. func (s *ServerService) UpdateGeofile(fileName string) error {
  1328. type geofileEntry struct {
  1329. URL string
  1330. FileName string
  1331. }
  1332. geofileAllowlist := map[string]geofileEntry{
  1333. "geoip.dat": {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip.dat"},
  1334. "geosite.dat": {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite.dat"},
  1335. "geoip_IR.dat": {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat", "geoip_IR.dat"},
  1336. "geosite_IR.dat": {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat", "geosite_IR.dat"},
  1337. "geoip_RU.dat": {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip_RU.dat"},
  1338. "geosite_RU.dat": {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite_RU.dat"},
  1339. }
  1340. // Strict allowlist check to avoid writing uncontrolled files
  1341. if fileName != "" {
  1342. if _, ok := geofileAllowlist[fileName]; !ok {
  1343. return common.NewErrorf("Invalid geofile name: %q not in allowlist", fileName)
  1344. }
  1345. }
  1346. client := s.settingService.NewProxiedHTTPClient(0)
  1347. downloadFile := func(url, destPath string) error {
  1348. var req *http.Request
  1349. req, err := http.NewRequest("GET", url, nil)
  1350. if err != nil {
  1351. return common.NewErrorf("Failed to create HTTP request for %s: %v", url, err)
  1352. }
  1353. var localFileModTime time.Time
  1354. if fileInfo, err := os.Stat(destPath); err == nil {
  1355. localFileModTime = fileInfo.ModTime()
  1356. if !localFileModTime.IsZero() {
  1357. req.Header.Set("If-Modified-Since", localFileModTime.UTC().Format(http.TimeFormat))
  1358. }
  1359. }
  1360. resp, err := client.Do(req)
  1361. if err != nil {
  1362. return common.NewErrorf("Failed to download Geofile from %s: %v", url, err)
  1363. }
  1364. defer resp.Body.Close()
  1365. // Parse Last-Modified header from server
  1366. var serverModTime time.Time
  1367. serverModTimeStr := resp.Header.Get("Last-Modified")
  1368. if serverModTimeStr != "" {
  1369. parsedTime, err := time.Parse(http.TimeFormat, serverModTimeStr)
  1370. if err != nil {
  1371. logger.Warningf("Failed to parse Last-Modified header for %s: %v", url, err)
  1372. } else {
  1373. serverModTime = parsedTime
  1374. }
  1375. }
  1376. // Function to update local file's modification time
  1377. updateFileModTime := func() {
  1378. if !serverModTime.IsZero() {
  1379. if err := os.Chtimes(destPath, serverModTime, serverModTime); err != nil {
  1380. logger.Warningf("Failed to update modification time for %s: %v", destPath, err)
  1381. }
  1382. }
  1383. }
  1384. // Handle 304 Not Modified
  1385. if resp.StatusCode == http.StatusNotModified {
  1386. updateFileModTime()
  1387. return nil
  1388. }
  1389. if resp.StatusCode != http.StatusOK {
  1390. return common.NewErrorf("Failed to download Geofile from %s: received status code %d", url, resp.StatusCode)
  1391. }
  1392. file, err := os.Create(destPath)
  1393. if err != nil {
  1394. return common.NewErrorf("Failed to create Geofile %s: %v", destPath, err)
  1395. }
  1396. defer file.Close()
  1397. _, err = io.Copy(file, resp.Body)
  1398. if err != nil {
  1399. return common.NewErrorf("Failed to save Geofile %s: %v", destPath, err)
  1400. }
  1401. updateFileModTime()
  1402. return nil
  1403. }
  1404. var errorMessages []string
  1405. if fileName == "" {
  1406. // Download all geofiles
  1407. for _, entry := range geofileAllowlist {
  1408. destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
  1409. if err := downloadFile(entry.URL, destPath); err != nil {
  1410. errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
  1411. }
  1412. }
  1413. } else {
  1414. entry := geofileAllowlist[fileName]
  1415. destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
  1416. if err := downloadFile(entry.URL, destPath); err != nil {
  1417. errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
  1418. }
  1419. }
  1420. err := s.RestartXrayService()
  1421. if err != nil {
  1422. errorMessages = append(errorMessages, fmt.Sprintf("Updated Geofile '%s' but Failed to start Xray: %v", fileName, err))
  1423. }
  1424. if len(errorMessages) > 0 {
  1425. return common.NewErrorf("%s", strings.Join(errorMessages, "\r\n"))
  1426. }
  1427. return nil
  1428. }
  1429. func (s *ServerService) GetNewX25519Cert() (any, error) {
  1430. // Run the command
  1431. cmd := exec.Command(xray.GetBinaryPath(), "x25519")
  1432. var out bytes.Buffer
  1433. cmd.Stdout = &out
  1434. err := cmd.Run()
  1435. if err != nil {
  1436. return nil, err
  1437. }
  1438. lines := strings.Split(out.String(), "\n")
  1439. privateKeyLine := strings.Split(lines[0], ":")
  1440. publicKeyLine := strings.Split(lines[1], ":")
  1441. privateKey := strings.TrimSpace(privateKeyLine[1])
  1442. publicKey := strings.TrimSpace(publicKeyLine[1])
  1443. keyPair := map[string]any{
  1444. "privateKey": privateKey,
  1445. "publicKey": publicKey,
  1446. }
  1447. return keyPair, nil
  1448. }
  1449. func (s *ServerService) GetNewmldsa65() (any, error) {
  1450. // Run the command
  1451. cmd := exec.Command(xray.GetBinaryPath(), "mldsa65")
  1452. var out bytes.Buffer
  1453. cmd.Stdout = &out
  1454. err := cmd.Run()
  1455. if err != nil {
  1456. return nil, err
  1457. }
  1458. lines := strings.Split(out.String(), "\n")
  1459. SeedLine := strings.Split(lines[0], ":")
  1460. VerifyLine := strings.Split(lines[1], ":")
  1461. seed := strings.TrimSpace(SeedLine[1])
  1462. verify := strings.TrimSpace(VerifyLine[1])
  1463. keyPair := map[string]any{
  1464. "seed": seed,
  1465. "verify": verify,
  1466. }
  1467. return keyPair, nil
  1468. }
  1469. func (s *ServerService) GetNewEchCert(sni string) (any, error) {
  1470. // Run the command
  1471. cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni)
  1472. var out bytes.Buffer
  1473. cmd.Stdout = &out
  1474. err := cmd.Run()
  1475. if err != nil {
  1476. return nil, err
  1477. }
  1478. lines := strings.Split(out.String(), "\n")
  1479. if len(lines) < 4 {
  1480. return nil, common.NewError("invalid ech cert")
  1481. }
  1482. configList := lines[1]
  1483. serverKeys := lines[3]
  1484. return map[string]any{
  1485. "echServerKeys": serverKeys,
  1486. "echConfigList": configList,
  1487. }, nil
  1488. }
  1489. func (s *ServerService) GetNewVlessEnc() (any, error) {
  1490. cmd := exec.Command(xray.GetBinaryPath(), "vlessenc")
  1491. var out bytes.Buffer
  1492. cmd.Stdout = &out
  1493. if err := cmd.Run(); err != nil {
  1494. return nil, err
  1495. }
  1496. return map[string]any{
  1497. "auths": parseVlessEncAuths(out.String()),
  1498. }, nil
  1499. }
  1500. func parseVlessEncAuths(output string) []map[string]string {
  1501. lines := strings.Split(output, "\n")
  1502. var auths []map[string]string
  1503. var current map[string]string
  1504. for _, line := range lines {
  1505. line = strings.TrimSpace(line)
  1506. if strings.HasPrefix(line, "Authentication:") {
  1507. if current != nil {
  1508. auths = append(auths, current)
  1509. }
  1510. label := strings.TrimSpace(strings.TrimPrefix(line, "Authentication:"))
  1511. current = map[string]string{
  1512. "id": vlessEncAuthID(label),
  1513. "label": label,
  1514. }
  1515. } else if strings.HasPrefix(line, `"decryption"`) || strings.HasPrefix(line, `"encryption"`) {
  1516. parts := strings.SplitN(line, ":", 2)
  1517. if len(parts) == 2 && current != nil {
  1518. key := strings.Trim(parts[0], `" `)
  1519. val := strings.TrimSpace(parts[1])
  1520. val = strings.TrimSuffix(val, ",")
  1521. val = strings.Trim(val, `" `)
  1522. current[key] = val
  1523. }
  1524. }
  1525. }
  1526. if current != nil {
  1527. auths = append(auths, current)
  1528. }
  1529. return auths
  1530. }
  1531. func vlessEncAuthID(label string) string {
  1532. normalized := strings.NewReplacer("-", "", "_", "", " ", "").Replace(strings.ToLower(label))
  1533. switch {
  1534. case strings.Contains(normalized, "mlkem768"):
  1535. return "mlkem768"
  1536. case strings.Contains(normalized, "x25519"):
  1537. return "x25519"
  1538. default:
  1539. return normalized
  1540. }
  1541. }
  1542. func (s *ServerService) GetNewUUID() (map[string]string, error) {
  1543. newUUID, err := uuid.NewRandom()
  1544. if err != nil {
  1545. return nil, fmt.Errorf("failed to generate UUID: %w", err)
  1546. }
  1547. return map[string]string{
  1548. "uuid": newUUID.String(),
  1549. }, nil
  1550. }
  1551. func (s *ServerService) GetNewmlkem768() (any, error) {
  1552. // Run the command
  1553. cmd := exec.Command(xray.GetBinaryPath(), "mlkem768")
  1554. var out bytes.Buffer
  1555. cmd.Stdout = &out
  1556. err := cmd.Run()
  1557. if err != nil {
  1558. return nil, err
  1559. }
  1560. lines := strings.Split(out.String(), "\n")
  1561. SeedLine := strings.Split(lines[0], ":")
  1562. ClientLine := strings.Split(lines[1], ":")
  1563. seed := strings.TrimSpace(SeedLine[1])
  1564. client := strings.TrimSpace(ClientLine[1])
  1565. keyPair := map[string]any{
  1566. "seed": seed,
  1567. "client": client,
  1568. }
  1569. return keyPair, nil
  1570. }