server.go 59 KB

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