1
0

server.go 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369
  1. package service
  2. import (
  3. "archive/zip"
  4. "bufio"
  5. "bytes"
  6. "encoding/json"
  7. "fmt"
  8. "io"
  9. "io/fs"
  10. "mime/multipart"
  11. "net/http"
  12. "os"
  13. "os/exec"
  14. "path/filepath"
  15. "regexp"
  16. "runtime"
  17. "strconv"
  18. "strings"
  19. "sync"
  20. "time"
  21. "github.com/mhsanaei/3x-ui/v2/config"
  22. "github.com/mhsanaei/3x-ui/v2/database"
  23. "github.com/mhsanaei/3x-ui/v2/logger"
  24. "github.com/mhsanaei/3x-ui/v2/util/common"
  25. "github.com/mhsanaei/3x-ui/v2/util/sys"
  26. "github.com/mhsanaei/3x-ui/v2/xray"
  27. "github.com/google/uuid"
  28. "github.com/shirou/gopsutil/v4/cpu"
  29. "github.com/shirou/gopsutil/v4/disk"
  30. "github.com/shirou/gopsutil/v4/host"
  31. "github.com/shirou/gopsutil/v4/load"
  32. "github.com/shirou/gopsutil/v4/mem"
  33. "github.com/shirou/gopsutil/v4/net"
  34. )
  35. // ProcessState represents the current state of a system process.
  36. type ProcessState string
  37. // Process state constants
  38. const (
  39. Running ProcessState = "running" // Process is running normally
  40. Stop ProcessState = "stop" // Process is stopped
  41. Error ProcessState = "error" // Process is in error state
  42. )
  43. // Status represents comprehensive system and application status information.
  44. // It includes CPU, memory, disk, network statistics, and Xray process status.
  45. type Status struct {
  46. T time.Time `json:"-"`
  47. Cpu float64 `json:"cpu"`
  48. CpuCores int `json:"cpuCores"`
  49. LogicalPro int `json:"logicalPro"`
  50. CpuSpeedMhz float64 `json:"cpuSpeedMhz"`
  51. Mem struct {
  52. Current uint64 `json:"current"`
  53. Total uint64 `json:"total"`
  54. } `json:"mem"`
  55. Swap struct {
  56. Current uint64 `json:"current"`
  57. Total uint64 `json:"total"`
  58. } `json:"swap"`
  59. Disk struct {
  60. Current uint64 `json:"current"`
  61. Total uint64 `json:"total"`
  62. } `json:"disk"`
  63. Xray struct {
  64. State ProcessState `json:"state"`
  65. ErrorMsg string `json:"errorMsg"`
  66. Version string `json:"version"`
  67. } `json:"xray"`
  68. Uptime uint64 `json:"uptime"`
  69. Loads []float64 `json:"loads"`
  70. TcpCount int `json:"tcpCount"`
  71. UdpCount int `json:"udpCount"`
  72. NetIO struct {
  73. Up uint64 `json:"up"`
  74. Down uint64 `json:"down"`
  75. } `json:"netIO"`
  76. NetTraffic struct {
  77. Sent uint64 `json:"sent"`
  78. Recv uint64 `json:"recv"`
  79. } `json:"netTraffic"`
  80. PublicIP struct {
  81. IPv4 string `json:"ipv4"`
  82. IPv6 string `json:"ipv6"`
  83. } `json:"publicIP"`
  84. AppStats struct {
  85. Threads uint32 `json:"threads"`
  86. Mem uint64 `json:"mem"`
  87. Uptime uint64 `json:"uptime"`
  88. } `json:"appStats"`
  89. }
  90. // Release represents information about a software release from GitHub.
  91. type Release struct {
  92. TagName string `json:"tag_name"` // The tag name of the release
  93. }
  94. // ServerService provides business logic for server monitoring and management.
  95. // It handles system status collection, IP detection, and application statistics.
  96. type ServerService struct {
  97. xrayService XrayService
  98. inboundService InboundService
  99. cachedIPv4 string
  100. cachedIPv6 string
  101. noIPv6 bool
  102. mu sync.Mutex
  103. lastCPUTimes cpu.TimesStat
  104. hasLastCPUSample bool
  105. hasNativeCPUSample bool
  106. emaCPU float64
  107. cpuHistory []CPUSample
  108. cachedCpuSpeedMhz float64
  109. lastCpuInfoAttempt time.Time
  110. }
  111. // AggregateCpuHistory returns up to maxPoints averaged buckets of size bucketSeconds over recent data.
  112. func (s *ServerService) AggregateCpuHistory(bucketSeconds int, maxPoints int) []map[string]any {
  113. if bucketSeconds <= 0 || maxPoints <= 0 {
  114. return nil
  115. }
  116. cutoff := time.Now().Add(-time.Duration(bucketSeconds*maxPoints) * time.Second).Unix()
  117. s.mu.Lock()
  118. // find start index (history sorted ascending)
  119. hist := s.cpuHistory
  120. // binary-ish scan (simple linear from end since size capped ~10800 is fine)
  121. startIdx := 0
  122. for i := len(hist) - 1; i >= 0; i-- {
  123. if hist[i].T < cutoff {
  124. startIdx = i + 1
  125. break
  126. }
  127. }
  128. if startIdx >= len(hist) {
  129. s.mu.Unlock()
  130. return []map[string]any{}
  131. }
  132. slice := hist[startIdx:]
  133. // copy for unlock
  134. tmp := make([]CPUSample, len(slice))
  135. copy(tmp, slice)
  136. s.mu.Unlock()
  137. if len(tmp) == 0 {
  138. return []map[string]any{}
  139. }
  140. var out []map[string]any
  141. var acc []float64
  142. bSize := int64(bucketSeconds)
  143. curBucket := (tmp[0].T / bSize) * bSize
  144. flush := func(ts int64) {
  145. if len(acc) == 0 {
  146. return
  147. }
  148. sum := 0.0
  149. for _, v := range acc {
  150. sum += v
  151. }
  152. avg := sum / float64(len(acc))
  153. out = append(out, map[string]any{"t": ts, "cpu": avg})
  154. acc = acc[:0]
  155. }
  156. for _, p := range tmp {
  157. b := (p.T / bSize) * bSize
  158. if b != curBucket {
  159. flush(curBucket)
  160. curBucket = b
  161. }
  162. acc = append(acc, p.Cpu)
  163. }
  164. flush(curBucket)
  165. if len(out) > maxPoints {
  166. out = out[len(out)-maxPoints:]
  167. }
  168. return out
  169. }
  170. // CPUSample single CPU utilization sample
  171. type CPUSample struct {
  172. T int64 `json:"t"` // unix seconds
  173. Cpu float64 `json:"cpu"` // percent 0..100
  174. }
  175. type LogEntry struct {
  176. DateTime time.Time
  177. FromAddress string
  178. ToAddress string
  179. Inbound string
  180. Outbound string
  181. Email string
  182. Event int
  183. }
  184. func getPublicIP(url string) string {
  185. client := &http.Client{
  186. Timeout: 3 * time.Second,
  187. }
  188. resp, err := client.Get(url)
  189. if err != nil {
  190. return "N/A"
  191. }
  192. defer resp.Body.Close()
  193. // Don't retry if access is blocked or region-restricted
  194. if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnavailableForLegalReasons {
  195. return "N/A"
  196. }
  197. if resp.StatusCode != http.StatusOK {
  198. return "N/A"
  199. }
  200. ip, err := io.ReadAll(resp.Body)
  201. if err != nil {
  202. return "N/A"
  203. }
  204. ipString := strings.TrimSpace(string(ip))
  205. if ipString == "" {
  206. return "N/A"
  207. }
  208. return ipString
  209. }
  210. func (s *ServerService) GetStatus(lastStatus *Status) *Status {
  211. now := time.Now()
  212. status := &Status{
  213. T: now,
  214. }
  215. // CPU stats
  216. util, err := s.sampleCPUUtilization()
  217. if err != nil {
  218. logger.Warning("get cpu percent failed:", err)
  219. } else {
  220. status.Cpu = util
  221. }
  222. status.CpuCores, err = cpu.Counts(false)
  223. if err != nil {
  224. logger.Warning("get cpu cores count failed:", err)
  225. }
  226. status.LogicalPro = runtime.NumCPU()
  227. if status.CpuSpeedMhz = s.cachedCpuSpeedMhz; s.cachedCpuSpeedMhz == 0 && time.Since(s.lastCpuInfoAttempt) > 5*time.Minute {
  228. s.lastCpuInfoAttempt = time.Now()
  229. done := make(chan struct{})
  230. go func() {
  231. defer close(done)
  232. cpuInfos, err := cpu.Info()
  233. if err != nil {
  234. logger.Warning("get cpu info failed:", err)
  235. return
  236. }
  237. if len(cpuInfos) > 0 {
  238. s.cachedCpuSpeedMhz = cpuInfos[0].Mhz
  239. status.CpuSpeedMhz = s.cachedCpuSpeedMhz
  240. } else {
  241. logger.Warning("could not find cpu info")
  242. }
  243. }()
  244. select {
  245. case <-done:
  246. case <-time.After(1500 * time.Millisecond):
  247. logger.Warning("cpu info query timed out; will retry later")
  248. }
  249. } else if s.cachedCpuSpeedMhz != 0 {
  250. status.CpuSpeedMhz = s.cachedCpuSpeedMhz
  251. }
  252. // Uptime
  253. upTime, err := host.Uptime()
  254. if err != nil {
  255. logger.Warning("get uptime failed:", err)
  256. } else {
  257. status.Uptime = upTime
  258. }
  259. // Memory stats
  260. memInfo, err := mem.VirtualMemory()
  261. if err != nil {
  262. logger.Warning("get virtual memory failed:", err)
  263. } else {
  264. status.Mem.Current = memInfo.Used
  265. status.Mem.Total = memInfo.Total
  266. }
  267. swapInfo, err := mem.SwapMemory()
  268. if err != nil {
  269. logger.Warning("get swap memory failed:", err)
  270. } else {
  271. status.Swap.Current = swapInfo.Used
  272. status.Swap.Total = swapInfo.Total
  273. }
  274. // Disk stats
  275. diskInfo, err := disk.Usage("/")
  276. if err != nil {
  277. logger.Warning("get disk usage failed:", err)
  278. } else {
  279. status.Disk.Current = diskInfo.Used
  280. status.Disk.Total = diskInfo.Total
  281. }
  282. // Load averages
  283. avgState, err := load.Avg()
  284. if err != nil {
  285. logger.Warning("get load avg failed:", err)
  286. } else {
  287. status.Loads = []float64{avgState.Load1, avgState.Load5, avgState.Load15}
  288. }
  289. // Network stats
  290. ioStats, err := net.IOCounters(true)
  291. if err != nil {
  292. logger.Warning("get io counters failed:", err)
  293. } else {
  294. var totalSent, totalRecv uint64
  295. for _, iface := range ioStats {
  296. name := strings.ToLower(iface.Name)
  297. if isVirtualInterface(name) {
  298. continue
  299. }
  300. totalSent += iface.BytesSent
  301. totalRecv += iface.BytesRecv
  302. }
  303. status.NetTraffic.Sent = totalSent
  304. status.NetTraffic.Recv = totalRecv
  305. if lastStatus != nil {
  306. duration := now.Sub(lastStatus.T)
  307. seconds := float64(duration) / float64(time.Second)
  308. up := uint64(float64(status.NetTraffic.Sent-lastStatus.NetTraffic.Sent) / seconds)
  309. down := uint64(float64(status.NetTraffic.Recv-lastStatus.NetTraffic.Recv) / seconds)
  310. status.NetIO.Up = up
  311. status.NetIO.Down = down
  312. }
  313. }
  314. // TCP/UDP connections
  315. status.TcpCount, err = sys.GetTCPCount()
  316. if err != nil {
  317. logger.Warning("get tcp connections failed:", err)
  318. }
  319. status.UdpCount, err = sys.GetUDPCount()
  320. if err != nil {
  321. logger.Warning("get udp connections failed:", err)
  322. }
  323. // IP fetching with caching
  324. showIp4ServiceLists := []string{
  325. "https://api4.ipify.org",
  326. "https://ipv4.icanhazip.com",
  327. "https://v4.api.ipinfo.io/ip",
  328. "https://ipv4.myexternalip.com/raw",
  329. "https://4.ident.me",
  330. "https://check-host.net/ip",
  331. }
  332. showIp6ServiceLists := []string{
  333. "https://api6.ipify.org",
  334. "https://ipv6.icanhazip.com",
  335. "https://v6.api.ipinfo.io/ip",
  336. "https://ipv6.myexternalip.com/raw",
  337. "https://6.ident.me",
  338. }
  339. if s.cachedIPv4 == "" {
  340. for _, ip4Service := range showIp4ServiceLists {
  341. s.cachedIPv4 = getPublicIP(ip4Service)
  342. if s.cachedIPv4 != "N/A" {
  343. break
  344. }
  345. }
  346. }
  347. if s.cachedIPv6 == "" && !s.noIPv6 {
  348. for _, ip6Service := range showIp6ServiceLists {
  349. s.cachedIPv6 = getPublicIP(ip6Service)
  350. if s.cachedIPv6 != "N/A" {
  351. break
  352. }
  353. }
  354. }
  355. if s.cachedIPv6 == "N/A" {
  356. s.noIPv6 = true
  357. }
  358. status.PublicIP.IPv4 = s.cachedIPv4
  359. status.PublicIP.IPv6 = s.cachedIPv6
  360. // Xray status
  361. if s.xrayService.IsXrayRunning() {
  362. status.Xray.State = Running
  363. status.Xray.ErrorMsg = ""
  364. } else {
  365. err := s.xrayService.GetXrayErr()
  366. if err != nil {
  367. status.Xray.State = Error
  368. } else {
  369. status.Xray.State = Stop
  370. }
  371. status.Xray.ErrorMsg = s.xrayService.GetXrayResult()
  372. }
  373. status.Xray.Version = s.xrayService.GetXrayVersion()
  374. // Application stats
  375. var rtm runtime.MemStats
  376. runtime.ReadMemStats(&rtm)
  377. status.AppStats.Mem = rtm.Sys
  378. status.AppStats.Threads = uint32(runtime.NumGoroutine())
  379. if p != nil && p.IsRunning() {
  380. status.AppStats.Uptime = p.GetUptime()
  381. } else {
  382. status.AppStats.Uptime = 0
  383. }
  384. return status
  385. }
  386. func (s *ServerService) AppendCpuSample(t time.Time, v float64) {
  387. const capacity = 9000 // ~5 hours @ 2s interval
  388. s.mu.Lock()
  389. defer s.mu.Unlock()
  390. p := CPUSample{T: t.Unix(), Cpu: v}
  391. if n := len(s.cpuHistory); n > 0 && s.cpuHistory[n-1].T == p.T {
  392. s.cpuHistory[n-1] = p
  393. } else {
  394. s.cpuHistory = append(s.cpuHistory, p)
  395. }
  396. if len(s.cpuHistory) > capacity {
  397. s.cpuHistory = s.cpuHistory[len(s.cpuHistory)-capacity:]
  398. }
  399. }
  400. func (s *ServerService) sampleCPUUtilization() (float64, error) {
  401. // Try native platform-specific CPU implementation first (Windows, Linux, macOS)
  402. if pct, err := sys.CPUPercentRaw(); err == nil {
  403. s.mu.Lock()
  404. // First call to native method returns 0 (initializes baseline)
  405. if !s.hasNativeCPUSample {
  406. s.hasNativeCPUSample = true
  407. s.mu.Unlock()
  408. return 0, nil
  409. }
  410. // Smooth with EMA
  411. const alpha = 0.3
  412. if s.emaCPU == 0 {
  413. s.emaCPU = pct
  414. } else {
  415. s.emaCPU = alpha*pct + (1-alpha)*s.emaCPU
  416. }
  417. val := s.emaCPU
  418. s.mu.Unlock()
  419. return val, nil
  420. }
  421. // If native call fails, fall back to gopsutil times
  422. // Read aggregate CPU times (all CPUs combined)
  423. times, err := cpu.Times(false)
  424. if err != nil {
  425. return 0, err
  426. }
  427. if len(times) == 0 {
  428. return 0, fmt.Errorf("no cpu times available")
  429. }
  430. cur := times[0]
  431. s.mu.Lock()
  432. defer s.mu.Unlock()
  433. // If this is the first sample, initialize and return current EMA (0 by default)
  434. if !s.hasLastCPUSample {
  435. s.lastCPUTimes = cur
  436. s.hasLastCPUSample = true
  437. return s.emaCPU, nil
  438. }
  439. // Compute busy and total deltas
  440. // Note: Guest and GuestNice times are already included in User and Nice respectively,
  441. // so we exclude them to avoid double-counting (Linux kernel accounting)
  442. idleDelta := cur.Idle - s.lastCPUTimes.Idle
  443. busyDelta := (cur.User - s.lastCPUTimes.User) +
  444. (cur.System - s.lastCPUTimes.System) +
  445. (cur.Nice - s.lastCPUTimes.Nice) +
  446. (cur.Iowait - s.lastCPUTimes.Iowait) +
  447. (cur.Irq - s.lastCPUTimes.Irq) +
  448. (cur.Softirq - s.lastCPUTimes.Softirq) +
  449. (cur.Steal - s.lastCPUTimes.Steal)
  450. totalDelta := busyDelta + idleDelta
  451. // Update last sample for next time
  452. s.lastCPUTimes = cur
  453. // Guard against division by zero or negative deltas (e.g., counter resets)
  454. if totalDelta <= 0 {
  455. return s.emaCPU, nil
  456. }
  457. raw := 100.0 * (busyDelta / totalDelta)
  458. if raw < 0 {
  459. raw = 0
  460. }
  461. if raw > 100 {
  462. raw = 100
  463. }
  464. // Exponential moving average to smooth spikes
  465. const alpha = 0.3 // smoothing factor (0<alpha<=1). Higher = more responsive, lower = smoother
  466. if s.emaCPU == 0 {
  467. // Initialize EMA with the first real reading to avoid long warm-up from zero
  468. s.emaCPU = raw
  469. } else {
  470. s.emaCPU = alpha*raw + (1-alpha)*s.emaCPU
  471. }
  472. return s.emaCPU, nil
  473. }
  474. func (s *ServerService) GetXrayVersions() ([]string, error) {
  475. const (
  476. XrayURL = "https://api.github.com/repos/XTLS/Xray-core/releases"
  477. bufferSize = 8192
  478. )
  479. resp, err := http.Get(XrayURL)
  480. if err != nil {
  481. return nil, err
  482. }
  483. defer resp.Body.Close()
  484. // Check HTTP status code - GitHub API returns object instead of array on error
  485. if resp.StatusCode != http.StatusOK {
  486. bodyBytes, _ := io.ReadAll(resp.Body)
  487. var errorResponse struct {
  488. Message string `json:"message"`
  489. }
  490. if json.Unmarshal(bodyBytes, &errorResponse) == nil && errorResponse.Message != "" {
  491. return nil, fmt.Errorf("GitHub API error: %s", errorResponse.Message)
  492. }
  493. return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, resp.Status)
  494. }
  495. buffer := bytes.NewBuffer(make([]byte, bufferSize))
  496. buffer.Reset()
  497. if _, err := buffer.ReadFrom(resp.Body); err != nil {
  498. return nil, err
  499. }
  500. var releases []Release
  501. if err := json.Unmarshal(buffer.Bytes(), &releases); err != nil {
  502. return nil, err
  503. }
  504. var versions []string
  505. for _, release := range releases {
  506. tagVersion := strings.TrimPrefix(release.TagName, "v")
  507. if tagVersion == "26.5.3" {
  508. continue
  509. }
  510. tagParts := strings.Split(tagVersion, ".")
  511. if len(tagParts) != 3 {
  512. continue
  513. }
  514. major, err1 := strconv.Atoi(tagParts[0])
  515. minor, err2 := strconv.Atoi(tagParts[1])
  516. patch, err3 := strconv.Atoi(tagParts[2])
  517. if err1 != nil || err2 != nil || err3 != nil {
  518. continue
  519. }
  520. if major > 26 || (major == 26 && minor > 4) || (major == 26 && minor == 4 && patch >= 25) {
  521. versions = append(versions, release.TagName)
  522. }
  523. }
  524. return versions, nil
  525. }
  526. func (s *ServerService) StopXrayService() error {
  527. err := s.xrayService.StopXray()
  528. if err != nil {
  529. logger.Error("stop xray failed:", err)
  530. return err
  531. }
  532. return nil
  533. }
  534. func (s *ServerService) RestartXrayService() error {
  535. err := s.xrayService.RestartXray(true)
  536. if err != nil {
  537. logger.Error("start xray failed:", err)
  538. return err
  539. }
  540. return nil
  541. }
  542. func (s *ServerService) downloadXRay(version string) (string, error) {
  543. osName := runtime.GOOS
  544. arch := runtime.GOARCH
  545. switch osName {
  546. case "darwin":
  547. osName = "macos"
  548. case "windows":
  549. osName = "windows"
  550. }
  551. switch arch {
  552. case "amd64":
  553. arch = "64"
  554. case "arm64":
  555. arch = "arm64-v8a"
  556. case "armv7":
  557. arch = "arm32-v7a"
  558. case "armv6":
  559. arch = "arm32-v6"
  560. case "armv5":
  561. arch = "arm32-v5"
  562. case "386":
  563. arch = "32"
  564. case "s390x":
  565. arch = "s390x"
  566. }
  567. fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
  568. url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
  569. resp, err := http.Get(url)
  570. if err != nil {
  571. return "", err
  572. }
  573. defer resp.Body.Close()
  574. os.Remove(fileName)
  575. file, err := os.Create(fileName)
  576. if err != nil {
  577. return "", err
  578. }
  579. defer file.Close()
  580. _, err = io.Copy(file, resp.Body)
  581. if err != nil {
  582. return "", err
  583. }
  584. return fileName, nil
  585. }
  586. func (s *ServerService) UpdateXray(version string) error {
  587. // 1. Stop xray before doing anything
  588. if err := s.StopXrayService(); err != nil {
  589. logger.Warning("failed to stop xray before update:", err)
  590. }
  591. // 2. Download the zip
  592. zipFileName, err := s.downloadXRay(version)
  593. if err != nil {
  594. return err
  595. }
  596. defer os.Remove(zipFileName)
  597. zipFile, err := os.Open(zipFileName)
  598. if err != nil {
  599. return err
  600. }
  601. defer zipFile.Close()
  602. stat, err := zipFile.Stat()
  603. if err != nil {
  604. return err
  605. }
  606. reader, err := zip.NewReader(zipFile, stat.Size())
  607. if err != nil {
  608. return err
  609. }
  610. // 3. Helper to extract files
  611. copyZipFile := func(zipName string, fileName string) error {
  612. zipFile, err := reader.Open(zipName)
  613. if err != nil {
  614. return err
  615. }
  616. defer zipFile.Close()
  617. os.MkdirAll(filepath.Dir(fileName), 0755)
  618. os.Remove(fileName)
  619. file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, fs.ModePerm)
  620. if err != nil {
  621. return err
  622. }
  623. defer file.Close()
  624. _, err = io.Copy(file, zipFile)
  625. return err
  626. }
  627. // 4. Extract correct binary
  628. if runtime.GOOS == "windows" {
  629. targetBinary := filepath.Join("bin", "xray-windows-amd64.exe")
  630. err = copyZipFile("xray.exe", targetBinary)
  631. } else {
  632. err = copyZipFile("xray", xray.GetBinaryPath())
  633. }
  634. if err != nil {
  635. return err
  636. }
  637. // 5. Restart xray
  638. if err := s.xrayService.RestartXray(true); err != nil {
  639. logger.Error("start xray failed:", err)
  640. return err
  641. }
  642. return nil
  643. }
  644. func (s *ServerService) GetLogs(count string, level string, syslog string) []string {
  645. c, _ := strconv.Atoi(count)
  646. var lines []string
  647. if syslog == "true" {
  648. // Check if running on Windows - journalctl is not available
  649. if runtime.GOOS == "windows" {
  650. return []string{"Syslog is not supported on Windows. Please use application logs instead by unchecking the 'Syslog' option."}
  651. }
  652. // Validate and sanitize count parameter
  653. countInt, err := strconv.Atoi(count)
  654. if err != nil || countInt < 1 || countInt > 10000 {
  655. return []string{"Invalid count parameter - must be a number between 1 and 10000"}
  656. }
  657. // Validate level parameter - only allow valid syslog levels
  658. validLevels := map[string]bool{
  659. "0": true, "emerg": true,
  660. "1": true, "alert": true,
  661. "2": true, "crit": true,
  662. "3": true, "err": true,
  663. "4": true, "warning": true,
  664. "5": true, "notice": true,
  665. "6": true, "info": true,
  666. "7": true, "debug": true,
  667. }
  668. if !validLevels[level] {
  669. return []string{"Invalid level parameter - must be a valid syslog level"}
  670. }
  671. // Use hardcoded command with validated parameters
  672. cmd := exec.Command("journalctl", "-u", "x-ui", "--no-pager", "-n", strconv.Itoa(countInt), "-p", level)
  673. var out bytes.Buffer
  674. cmd.Stdout = &out
  675. err = cmd.Run()
  676. if err != nil {
  677. return []string{"Failed to run journalctl command! Make sure systemd is available and x-ui service is registered."}
  678. }
  679. lines = strings.Split(out.String(), "\n")
  680. } else {
  681. lines = logger.GetLogs(c, level)
  682. }
  683. return lines
  684. }
  685. func (s *ServerService) GetXrayLogs(
  686. count string,
  687. filter string,
  688. showDirect string,
  689. showBlocked string,
  690. showProxy string,
  691. freedoms []string,
  692. blackholes []string) []LogEntry {
  693. const (
  694. Direct = iota
  695. Blocked
  696. Proxied
  697. )
  698. countInt, _ := strconv.Atoi(count)
  699. var entries []LogEntry
  700. pathToAccessLog, err := xray.GetAccessLogPath()
  701. if err != nil {
  702. return nil
  703. }
  704. file, err := os.Open(pathToAccessLog)
  705. if err != nil {
  706. return nil
  707. }
  708. defer file.Close()
  709. scanner := bufio.NewScanner(file)
  710. for scanner.Scan() {
  711. line := strings.TrimSpace(scanner.Text())
  712. if line == "" || strings.Contains(line, "api -> api") {
  713. //skipping empty lines and api calls
  714. continue
  715. }
  716. if filter != "" && !strings.Contains(line, filter) {
  717. //applying filter if it's not empty
  718. continue
  719. }
  720. var entry LogEntry
  721. parts := strings.Fields(line)
  722. for i, part := range parts {
  723. if i == 0 {
  724. dateTime, err := time.ParseInLocation("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1], time.Local)
  725. if err != nil {
  726. continue
  727. }
  728. entry.DateTime = dateTime.UTC()
  729. }
  730. if part == "from" {
  731. entry.FromAddress = strings.TrimLeft(parts[i+1], "/")
  732. } else if part == "accepted" {
  733. entry.ToAddress = strings.TrimLeft(parts[i+1], "/")
  734. } else if strings.HasPrefix(part, "[") {
  735. entry.Inbound = part[1:]
  736. } else if strings.HasSuffix(part, "]") {
  737. entry.Outbound = part[:len(part)-1]
  738. } else if part == "email:" {
  739. entry.Email = parts[i+1]
  740. }
  741. }
  742. if logEntryContains(line, freedoms) {
  743. if showDirect == "false" {
  744. continue
  745. }
  746. entry.Event = Direct
  747. } else if logEntryContains(line, blackholes) {
  748. if showBlocked == "false" {
  749. continue
  750. }
  751. entry.Event = Blocked
  752. } else {
  753. if showProxy == "false" {
  754. continue
  755. }
  756. entry.Event = Proxied
  757. }
  758. entries = append(entries, entry)
  759. }
  760. if err := scanner.Err(); err != nil {
  761. return nil
  762. }
  763. if len(entries) > countInt {
  764. entries = entries[len(entries)-countInt:]
  765. }
  766. return entries
  767. }
  768. // isVirtualInterface returns true for loopback and virtual/tunnel interfaces
  769. // that should be excluded from network traffic statistics.
  770. func isVirtualInterface(name string) bool {
  771. // Exact matches
  772. if name == "lo" || name == "lo0" {
  773. return true
  774. }
  775. // Prefix matches for virtual/tunnel interfaces
  776. virtualPrefixes := []string{
  777. "loopback",
  778. "docker",
  779. "br-",
  780. "veth",
  781. "virbr",
  782. "tun",
  783. "tap",
  784. "wg",
  785. "tailscale",
  786. "zt",
  787. }
  788. for _, prefix := range virtualPrefixes {
  789. if strings.HasPrefix(name, prefix) {
  790. return true
  791. }
  792. }
  793. return false
  794. }
  795. func logEntryContains(line string, suffixes []string) bool {
  796. for _, sfx := range suffixes {
  797. if strings.Contains(line, sfx+"]") {
  798. return true
  799. }
  800. }
  801. return false
  802. }
  803. func (s *ServerService) GetConfigJson() (any, error) {
  804. config, err := s.xrayService.GetXrayConfig()
  805. if err != nil {
  806. return nil, err
  807. }
  808. contents, err := json.MarshalIndent(config, "", " ")
  809. if err != nil {
  810. return nil, err
  811. }
  812. var jsonData any
  813. err = json.Unmarshal(contents, &jsonData)
  814. if err != nil {
  815. return nil, err
  816. }
  817. return jsonData, nil
  818. }
  819. func (s *ServerService) GetDb() ([]byte, error) {
  820. // Update by manually trigger a checkpoint operation
  821. err := database.Checkpoint()
  822. if err != nil {
  823. return nil, err
  824. }
  825. // Open the file for reading
  826. file, err := os.Open(config.GetDBPath())
  827. if err != nil {
  828. return nil, err
  829. }
  830. defer file.Close()
  831. // Read the file contents
  832. fileContents, err := io.ReadAll(file)
  833. if err != nil {
  834. return nil, err
  835. }
  836. return fileContents, nil
  837. }
  838. func (s *ServerService) ImportDB(file multipart.File) error {
  839. // Check if the file is a SQLite database
  840. isValidDb, err := database.IsSQLiteDB(file)
  841. if err != nil {
  842. return common.NewErrorf("Error checking db file format: %v", err)
  843. }
  844. if !isValidDb {
  845. return common.NewError("Invalid db file format")
  846. }
  847. // Reset the file reader to the beginning
  848. _, err = file.Seek(0, 0)
  849. if err != nil {
  850. return common.NewErrorf("Error resetting file reader: %v", err)
  851. }
  852. // Save the file as a temporary file
  853. tempPath := fmt.Sprintf("%s.temp", config.GetDBPath())
  854. // Remove the existing temporary file (if any)
  855. if _, err := os.Stat(tempPath); err == nil {
  856. if errRemove := os.Remove(tempPath); errRemove != nil {
  857. return common.NewErrorf("Error removing existing temporary db file: %v", errRemove)
  858. }
  859. }
  860. // Create the temporary file
  861. tempFile, err := os.Create(tempPath)
  862. if err != nil {
  863. return common.NewErrorf("Error creating temporary db file: %v", err)
  864. }
  865. // Robust deferred cleanup for the temporary file
  866. defer func() {
  867. if tempFile != nil {
  868. if cerr := tempFile.Close(); cerr != nil {
  869. logger.Warningf("Warning: failed to close temp file: %v", cerr)
  870. }
  871. }
  872. if _, err := os.Stat(tempPath); err == nil {
  873. if rerr := os.Remove(tempPath); rerr != nil {
  874. logger.Warningf("Warning: failed to remove temp file: %v", rerr)
  875. }
  876. }
  877. }()
  878. // Save uploaded file to temporary file
  879. if _, err = io.Copy(tempFile, file); err != nil {
  880. return common.NewErrorf("Error saving db: %v", err)
  881. }
  882. // Close temp file before opening via sqlite
  883. if err = tempFile.Close(); err != nil {
  884. return common.NewErrorf("Error closing temporary db file: %v", err)
  885. }
  886. tempFile = nil
  887. // Validate integrity (no migrations / side effects)
  888. if err = database.ValidateSQLiteDB(tempPath); err != nil {
  889. return common.NewErrorf("Invalid or corrupt db file: %v", err)
  890. }
  891. // Stop Xray (ignore error but log)
  892. if errStop := s.StopXrayService(); errStop != nil {
  893. logger.Warningf("Failed to stop Xray before DB import: %v", errStop)
  894. }
  895. // Close existing DB to release file locks (especially on Windows)
  896. if errClose := database.CloseDB(); errClose != nil {
  897. logger.Warningf("Failed to close existing DB before replacement: %v", errClose)
  898. }
  899. // Backup the current database for fallback
  900. fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
  901. // Remove the existing fallback file (if any)
  902. if _, err := os.Stat(fallbackPath); err == nil {
  903. if errRemove := os.Remove(fallbackPath); errRemove != nil {
  904. return common.NewErrorf("Error removing existing fallback db file: %v", errRemove)
  905. }
  906. }
  907. // Move the current database to the fallback location
  908. if err = os.Rename(config.GetDBPath(), fallbackPath); err != nil {
  909. return common.NewErrorf("Error backing up current db file: %v", err)
  910. }
  911. // Defer fallback cleanup ONLY if everything goes well
  912. defer func() {
  913. if _, err := os.Stat(fallbackPath); err == nil {
  914. if rerr := os.Remove(fallbackPath); rerr != nil {
  915. logger.Warningf("Warning: failed to remove fallback file: %v", rerr)
  916. }
  917. }
  918. }()
  919. // Move temp to DB path
  920. if err = os.Rename(tempPath, config.GetDBPath()); err != nil {
  921. // Restore from fallback
  922. if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil {
  923. return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename)
  924. }
  925. return common.NewErrorf("Error moving db file: %v", err)
  926. }
  927. // Open & migrate new DB
  928. if err = database.InitDB(config.GetDBPath()); err != nil {
  929. if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil {
  930. return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
  931. }
  932. return common.NewErrorf("Error migrating db: %v", err)
  933. }
  934. s.inboundService.MigrateDB()
  935. // Start Xray
  936. if err = s.RestartXrayService(); err != nil {
  937. return common.NewErrorf("Imported DB but failed to start Xray: %v", err)
  938. }
  939. return nil
  940. }
  941. // IsValidGeofileName validates that the filename is safe for geofile operations.
  942. // It checks for path traversal attempts and ensures the filename contains only safe characters.
  943. func (s *ServerService) IsValidGeofileName(filename string) bool {
  944. if filename == "" {
  945. return false
  946. }
  947. // Check for path traversal attempts
  948. if strings.Contains(filename, "..") {
  949. return false
  950. }
  951. // Check for path separators (both forward and backward slash)
  952. if strings.ContainsAny(filename, `/\`) {
  953. return false
  954. }
  955. // Check for absolute path indicators
  956. if filepath.IsAbs(filename) {
  957. return false
  958. }
  959. // Additional security: only allow alphanumeric, dots, underscores, and hyphens
  960. // This is stricter than the general filename regex
  961. validGeofilePattern := `^[a-zA-Z0-9._-]+\.dat$`
  962. matched, _ := regexp.MatchString(validGeofilePattern, filename)
  963. return matched
  964. }
  965. func (s *ServerService) UpdateGeofile(fileName string) error {
  966. type geofileEntry struct {
  967. URL string
  968. FileName string
  969. }
  970. geofileAllowlist := map[string]geofileEntry{
  971. "geoip.dat": {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip.dat"},
  972. "geosite.dat": {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite.dat"},
  973. "geoip_IR.dat": {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat", "geoip_IR.dat"},
  974. "geosite_IR.dat": {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat", "geosite_IR.dat"},
  975. "geoip_RU.dat": {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip_RU.dat"},
  976. "geosite_RU.dat": {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite_RU.dat"},
  977. }
  978. // Strict allowlist check to avoid writing uncontrolled files
  979. if fileName != "" {
  980. if _, ok := geofileAllowlist[fileName]; !ok {
  981. return common.NewErrorf("Invalid geofile name: %q not in allowlist", fileName)
  982. }
  983. }
  984. downloadFile := func(url, destPath string) error {
  985. var req *http.Request
  986. req, err := http.NewRequest("GET", url, nil)
  987. if err != nil {
  988. return common.NewErrorf("Failed to create HTTP request for %s: %v", url, err)
  989. }
  990. var localFileModTime time.Time
  991. if fileInfo, err := os.Stat(destPath); err == nil {
  992. localFileModTime = fileInfo.ModTime()
  993. if !localFileModTime.IsZero() {
  994. req.Header.Set("If-Modified-Since", localFileModTime.UTC().Format(http.TimeFormat))
  995. }
  996. }
  997. client := &http.Client{}
  998. resp, err := client.Do(req)
  999. if err != nil {
  1000. return common.NewErrorf("Failed to download Geofile from %s: %v", url, err)
  1001. }
  1002. defer resp.Body.Close()
  1003. // Parse Last-Modified header from server
  1004. var serverModTime time.Time
  1005. serverModTimeStr := resp.Header.Get("Last-Modified")
  1006. if serverModTimeStr != "" {
  1007. parsedTime, err := time.Parse(http.TimeFormat, serverModTimeStr)
  1008. if err != nil {
  1009. logger.Warningf("Failed to parse Last-Modified header for %s: %v", url, err)
  1010. } else {
  1011. serverModTime = parsedTime
  1012. }
  1013. }
  1014. // Function to update local file's modification time
  1015. updateFileModTime := func() {
  1016. if !serverModTime.IsZero() {
  1017. if err := os.Chtimes(destPath, serverModTime, serverModTime); err != nil {
  1018. logger.Warningf("Failed to update modification time for %s: %v", destPath, err)
  1019. }
  1020. }
  1021. }
  1022. // Handle 304 Not Modified
  1023. if resp.StatusCode == http.StatusNotModified {
  1024. updateFileModTime()
  1025. return nil
  1026. }
  1027. if resp.StatusCode != http.StatusOK {
  1028. return common.NewErrorf("Failed to download Geofile from %s: received status code %d", url, resp.StatusCode)
  1029. }
  1030. file, err := os.Create(destPath)
  1031. if err != nil {
  1032. return common.NewErrorf("Failed to create Geofile %s: %v", destPath, err)
  1033. }
  1034. defer file.Close()
  1035. _, err = io.Copy(file, resp.Body)
  1036. if err != nil {
  1037. return common.NewErrorf("Failed to save Geofile %s: %v", destPath, err)
  1038. }
  1039. updateFileModTime()
  1040. return nil
  1041. }
  1042. var errorMessages []string
  1043. if fileName == "" {
  1044. // Download all geofiles
  1045. for _, entry := range geofileAllowlist {
  1046. destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
  1047. if err := downloadFile(entry.URL, destPath); err != nil {
  1048. errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
  1049. }
  1050. }
  1051. } else {
  1052. entry := geofileAllowlist[fileName]
  1053. destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
  1054. if err := downloadFile(entry.URL, destPath); err != nil {
  1055. errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
  1056. }
  1057. }
  1058. err := s.RestartXrayService()
  1059. if err != nil {
  1060. errorMessages = append(errorMessages, fmt.Sprintf("Updated Geofile '%s' but Failed to start Xray: %v", fileName, err))
  1061. }
  1062. if len(errorMessages) > 0 {
  1063. return common.NewErrorf("%s", strings.Join(errorMessages, "\r\n"))
  1064. }
  1065. return nil
  1066. }
  1067. func (s *ServerService) GetNewX25519Cert() (any, error) {
  1068. // Run the command
  1069. cmd := exec.Command(xray.GetBinaryPath(), "x25519")
  1070. var out bytes.Buffer
  1071. cmd.Stdout = &out
  1072. err := cmd.Run()
  1073. if err != nil {
  1074. return nil, err
  1075. }
  1076. lines := strings.Split(out.String(), "\n")
  1077. privateKeyLine := strings.Split(lines[0], ":")
  1078. publicKeyLine := strings.Split(lines[1], ":")
  1079. privateKey := strings.TrimSpace(privateKeyLine[1])
  1080. publicKey := strings.TrimSpace(publicKeyLine[1])
  1081. keyPair := map[string]any{
  1082. "privateKey": privateKey,
  1083. "publicKey": publicKey,
  1084. }
  1085. return keyPair, nil
  1086. }
  1087. func (s *ServerService) GetNewmldsa65() (any, error) {
  1088. // Run the command
  1089. cmd := exec.Command(xray.GetBinaryPath(), "mldsa65")
  1090. var out bytes.Buffer
  1091. cmd.Stdout = &out
  1092. err := cmd.Run()
  1093. if err != nil {
  1094. return nil, err
  1095. }
  1096. lines := strings.Split(out.String(), "\n")
  1097. SeedLine := strings.Split(lines[0], ":")
  1098. VerifyLine := strings.Split(lines[1], ":")
  1099. seed := strings.TrimSpace(SeedLine[1])
  1100. verify := strings.TrimSpace(VerifyLine[1])
  1101. keyPair := map[string]any{
  1102. "seed": seed,
  1103. "verify": verify,
  1104. }
  1105. return keyPair, nil
  1106. }
  1107. func (s *ServerService) GetNewEchCert(sni string) (any, error) {
  1108. // Run the command
  1109. cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni)
  1110. var out bytes.Buffer
  1111. cmd.Stdout = &out
  1112. err := cmd.Run()
  1113. if err != nil {
  1114. return nil, err
  1115. }
  1116. lines := strings.Split(out.String(), "\n")
  1117. if len(lines) < 4 {
  1118. return nil, common.NewError("invalid ech cert")
  1119. }
  1120. configList := lines[1]
  1121. serverKeys := lines[3]
  1122. return map[string]any{
  1123. "echServerKeys": serverKeys,
  1124. "echConfigList": configList,
  1125. }, nil
  1126. }
  1127. func (s *ServerService) GetNewVlessEnc() (any, error) {
  1128. cmd := exec.Command(xray.GetBinaryPath(), "vlessenc")
  1129. var out bytes.Buffer
  1130. cmd.Stdout = &out
  1131. if err := cmd.Run(); err != nil {
  1132. return nil, err
  1133. }
  1134. lines := strings.Split(out.String(), "\n")
  1135. var auths []map[string]string
  1136. var current map[string]string
  1137. for _, line := range lines {
  1138. line = strings.TrimSpace(line)
  1139. if strings.HasPrefix(line, "Authentication:") {
  1140. if current != nil {
  1141. auths = append(auths, current)
  1142. }
  1143. current = map[string]string{
  1144. "label": strings.TrimSpace(strings.TrimPrefix(line, "Authentication:")),
  1145. }
  1146. } else if strings.HasPrefix(line, `"decryption"`) || strings.HasPrefix(line, `"encryption"`) {
  1147. parts := strings.SplitN(line, ":", 2)
  1148. if len(parts) == 2 && current != nil {
  1149. key := strings.Trim(parts[0], `" `)
  1150. val := strings.Trim(parts[1], `" `)
  1151. current[key] = val
  1152. }
  1153. }
  1154. }
  1155. if current != nil {
  1156. auths = append(auths, current)
  1157. }
  1158. return map[string]any{
  1159. "auths": auths,
  1160. }, nil
  1161. }
  1162. func (s *ServerService) GetNewUUID() (map[string]string, error) {
  1163. newUUID, err := uuid.NewRandom()
  1164. if err != nil {
  1165. return nil, fmt.Errorf("failed to generate UUID: %w", err)
  1166. }
  1167. return map[string]string{
  1168. "uuid": newUUID.String(),
  1169. }, nil
  1170. }
  1171. func (s *ServerService) GetNewmlkem768() (any, error) {
  1172. // Run the command
  1173. cmd := exec.Command(xray.GetBinaryPath(), "mlkem768")
  1174. var out bytes.Buffer
  1175. cmd.Stdout = &out
  1176. err := cmd.Run()
  1177. if err != nil {
  1178. return nil, err
  1179. }
  1180. lines := strings.Split(out.String(), "\n")
  1181. SeedLine := strings.Split(lines[0], ":")
  1182. ClientLine := strings.Split(lines[1], ":")
  1183. seed := strings.TrimSpace(SeedLine[1])
  1184. client := strings.TrimSpace(ClientLine[1])
  1185. keyPair := map[string]any{
  1186. "seed": seed,
  1187. "client": client,
  1188. }
  1189. return keyPair, nil
  1190. }