1
0

server.go 47 KB

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