server.go 51 KB

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