server.go 63 KB

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