1
0

node.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. package service
  2. import (
  3. "context"
  4. "crypto/sha256"
  5. "crypto/tls"
  6. "encoding/base64"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "net"
  11. "net/http"
  12. "net/url"
  13. "strconv"
  14. "strings"
  15. "time"
  16. "github.com/mhsanaei/3x-ui/v3/internal/database"
  17. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  18. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  19. "github.com/mhsanaei/3x-ui/v3/internal/util/netsafe"
  20. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  21. )
  22. type HeartbeatPatch struct {
  23. Status string
  24. LastHeartbeat int64
  25. LatencyMs int
  26. XrayVersion string
  27. PanelVersion string
  28. Guid string
  29. CpuPct float64
  30. MemPct float64
  31. UptimeSecs uint64
  32. LastError string
  33. // XrayState and XrayError come from the remote /panel/api/server/status when the
  34. // panel API is reachable. They allow distinguishing panel connectivity from
  35. // Xray core health on the node.
  36. XrayState string
  37. XrayError string
  38. }
  39. type NodeService struct{}
  40. // FetchCertFingerprint connects to the node over HTTPS without verifying the
  41. // certificate and returns the leaf certificate's SHA-256 as base64, so the UI
  42. // can offer a "fetch and pin current certificate" action.
  43. func (s *NodeService) FetchCertFingerprint(ctx context.Context, n *model.Node) (string, error) {
  44. addr, err := netsafe.NormalizeHost(n.Address)
  45. if err != nil {
  46. return "", err
  47. }
  48. scheme := n.Scheme
  49. if scheme != "http" && scheme != "https" {
  50. scheme = "https"
  51. }
  52. if scheme != "https" {
  53. return "", common.NewError("certificate pinning is only available for https nodes")
  54. }
  55. if n.Port <= 0 || n.Port > 65535 {
  56. return "", common.NewError("node port must be 1-65535")
  57. }
  58. probeURL := &url.URL{
  59. Scheme: scheme,
  60. Host: net.JoinHostPort(addr, strconv.Itoa(n.Port)),
  61. Path: normalizeBasePath(n.BasePath) + "panel/api/server/status",
  62. }
  63. req, err := http.NewRequestWithContext(
  64. netsafe.ContextWithAllowPrivate(ctx, n.AllowPrivateAddress),
  65. http.MethodGet, probeURL.String(), nil)
  66. if err != nil {
  67. return "", err
  68. }
  69. client := &http.Client{
  70. Transport: &http.Transport{
  71. DialContext: netsafe.SSRFGuardedDialContext,
  72. TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // lgtm[go/disabled-certificate-check]
  73. },
  74. }
  75. resp, err := client.Do(req)
  76. if err != nil {
  77. return "", err
  78. }
  79. defer resp.Body.Close()
  80. if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 {
  81. return "", common.NewError("node did not present a TLS certificate")
  82. }
  83. sum := sha256.Sum256(resp.TLS.PeerCertificates[0].Raw)
  84. return base64.StdEncoding.EncodeToString(sum[:]), nil
  85. }
  86. func (s *NodeService) GetAll() ([]*model.Node, error) {
  87. db := database.GetDB()
  88. var nodes []*model.Node
  89. err := db.Model(model.Node{}).Order("id asc").Find(&nodes).Error
  90. if err != nil || len(nodes) == 0 {
  91. return nodes, err
  92. }
  93. type inboundRow struct {
  94. Id int
  95. NodeID int `gorm:"column:node_id"`
  96. }
  97. var inboundRows []inboundRow
  98. if err := db.Table("inbounds").
  99. Select("id, node_id").
  100. Where("node_id IS NOT NULL").
  101. Scan(&inboundRows).Error; err != nil {
  102. return nodes, nil
  103. }
  104. if len(inboundRows) == 0 {
  105. return nodes, nil
  106. }
  107. inboundsByNode := make(map[int][]int, len(nodes))
  108. nodeByInbound := make(map[int]int, len(inboundRows))
  109. for _, row := range inboundRows {
  110. inboundsByNode[row.NodeID] = append(inboundsByNode[row.NodeID], row.Id)
  111. nodeByInbound[row.Id] = row.NodeID
  112. }
  113. type clientCountRow struct {
  114. NodeID int `gorm:"column:node_id"`
  115. Count int `gorm:"column:count"`
  116. }
  117. var clientCounts []clientCountRow
  118. if err := db.Raw(`
  119. SELECT inbounds.node_id AS node_id, COUNT(DISTINCT client_inbounds.client_id) AS count
  120. FROM inbounds
  121. JOIN client_inbounds ON client_inbounds.inbound_id = inbounds.id
  122. WHERE inbounds.node_id IS NOT NULL
  123. GROUP BY inbounds.node_id
  124. `).Scan(&clientCounts).Error; err == nil {
  125. for _, row := range clientCounts {
  126. for _, n := range nodes {
  127. if n.Id == row.NodeID {
  128. n.ClientCount = row.Count
  129. break
  130. }
  131. }
  132. }
  133. }
  134. now := time.Now().UnixMilli()
  135. type trafficRow struct {
  136. InboundID int `gorm:"column:inbound_id"`
  137. Email string
  138. Enable bool
  139. Total int64
  140. Up int64
  141. Down int64
  142. ExpiryTime int64 `gorm:"column:expiry_time"`
  143. }
  144. var trafficRows []trafficRow
  145. inboundIDs := make([]int, 0, len(nodeByInbound))
  146. for id := range nodeByInbound {
  147. inboundIDs = append(inboundIDs, id)
  148. }
  149. // Chunk the IN clause to avoid "too many SQL variables" on SQLite
  150. // when there are many node-owned inbounds (common with many nodes).
  151. // sqliteMaxVars is defined in this package (inbound.go).
  152. for _, batch := range chunkInts(inboundIDs, sqliteMaxVars) {
  153. var page []trafficRow
  154. if err := db.Table("client_traffics").
  155. Select("inbound_id, email, enable, total, up, down, expiry_time").
  156. Where("inbound_id IN ?", batch).
  157. Scan(&page).Error; err == nil {
  158. trafficRows = append(trafficRows, page...)
  159. }
  160. }
  161. depletedByNode := make(map[int]int)
  162. if len(trafficRows) > 0 {
  163. for _, row := range trafficRows {
  164. nodeID, ok := nodeByInbound[row.InboundID]
  165. if !ok {
  166. continue
  167. }
  168. expired := row.ExpiryTime > 0 && row.ExpiryTime <= now
  169. exhausted := row.Total > 0 && row.Up+row.Down >= row.Total
  170. if expired || exhausted || !row.Enable {
  171. depletedByNode[nodeID]++
  172. }
  173. }
  174. }
  175. onlineByGuid := s.onlineEmailsByGuid()
  176. for _, n := range nodes {
  177. n.InboundCount = len(inboundsByNode[n.Id])
  178. n.DepletedCount = depletedByNode[n.Id]
  179. // Online is attributed to the node that physically hosts the client
  180. // (by GUID): a client on a sub-node counts under the sub-node, not
  181. // the intermediate node it syncs through (#4983).
  182. n.OnlineCount = len(onlineByGuid[effectiveNodeGuid(n)])
  183. }
  184. return nodes, nil
  185. }
  186. func (s *NodeService) onlineEmailsByGuid() map[string]map[string]struct{} {
  187. svc := InboundService{}
  188. byGuid := svc.GetOnlineClientsByGuid()
  189. out := make(map[string]map[string]struct{}, len(byGuid))
  190. for guid, emails := range byGuid {
  191. set := make(map[string]struct{}, len(emails))
  192. for _, email := range emails {
  193. set[email] = struct{}{}
  194. }
  195. out[guid] = set
  196. }
  197. return out
  198. }
  199. // effectiveNodeGuid is a node's stable online-attribution key: its reported
  200. // panelGuid, or a master-local synthetic id when the node is an old build that
  201. // hasn't reported one yet (#4983).
  202. func effectiveNodeGuid(n *model.Node) string {
  203. if n.Guid != "" {
  204. return n.Guid
  205. }
  206. return synthNodeGuid(n.Id)
  207. }
  208. func (s *NodeService) GetById(id int) (*model.Node, error) {
  209. db := database.GetDB()
  210. n := &model.Node{}
  211. if err := db.Model(model.Node{}).Where("id = ?", id).First(n).Error; err != nil {
  212. return nil, err
  213. }
  214. return n, nil
  215. }
  216. // NodeExists reports whether a node with the given id exists on this panel.
  217. // Used to drop stale, cross-panel node references on inbound import. A Count
  218. // query distinguishes "no such node" (count 0, no error) from a real DB error.
  219. func (s *NodeService) NodeExists(id int) (bool, error) {
  220. if id <= 0 {
  221. return false, nil
  222. }
  223. var count int64
  224. if err := database.GetDB().Model(model.Node{}).Where("id = ?", id).Count(&count).Error; err != nil {
  225. return false, err
  226. }
  227. return count > 0, nil
  228. }
  229. func normalizeBasePath(p string) string {
  230. p = strings.TrimSpace(p)
  231. if p == "" {
  232. return "/"
  233. }
  234. if !strings.HasPrefix(p, "/") {
  235. p = "/" + p
  236. }
  237. if !strings.HasSuffix(p, "/") {
  238. p = p + "/"
  239. }
  240. return p
  241. }
  242. func (s *NodeService) normalize(n *model.Node) error {
  243. n.Name = strings.TrimSpace(n.Name)
  244. n.ApiToken = strings.TrimSpace(n.ApiToken)
  245. if n.Name == "" {
  246. return common.NewError("node name is required")
  247. }
  248. addr, err := netsafe.NormalizeHost(n.Address)
  249. if err != nil {
  250. return common.NewError(err.Error())
  251. }
  252. n.Address = addr
  253. if n.Port <= 0 || n.Port > 65535 {
  254. return common.NewError("node port must be 1-65535")
  255. }
  256. if n.Scheme != "http" && n.Scheme != "https" {
  257. n.Scheme = "https"
  258. }
  259. if n.TlsVerifyMode != "skip" && n.TlsVerifyMode != "pin" {
  260. n.TlsVerifyMode = "verify"
  261. }
  262. n.PinnedCertSha256 = strings.TrimSpace(n.PinnedCertSha256)
  263. if n.InboundSyncMode != "selected" {
  264. n.InboundSyncMode = "all"
  265. n.InboundTags = nil
  266. } else {
  267. seen := make(map[string]struct{}, len(n.InboundTags))
  268. tags := make([]string, 0, len(n.InboundTags))
  269. for _, tag := range n.InboundTags {
  270. tag = strings.TrimSpace(tag)
  271. if tag == "" {
  272. continue
  273. }
  274. if _, ok := seen[tag]; ok {
  275. continue
  276. }
  277. seen[tag] = struct{}{}
  278. tags = append(tags, tag)
  279. }
  280. n.InboundTags = tags
  281. }
  282. if n.TlsVerifyMode == "pin" {
  283. if _, err := runtime.DecodeCertPin(n.PinnedCertSha256); err != nil {
  284. return common.NewError(err.Error())
  285. }
  286. }
  287. n.BasePath = normalizeBasePath(n.BasePath)
  288. return nil
  289. }
  290. func (s *NodeService) Create(n *model.Node) error {
  291. if err := s.normalize(n); err != nil {
  292. return err
  293. }
  294. db := database.GetDB()
  295. return db.Create(n).Error
  296. }
  297. func (s *NodeService) Update(id int, in *model.Node) error {
  298. if err := s.normalize(in); err != nil {
  299. return err
  300. }
  301. inboundTagsJSON, err := json.Marshal(in.InboundTags)
  302. if err != nil {
  303. return err
  304. }
  305. db := database.GetDB()
  306. existing := &model.Node{}
  307. if err := db.Where("id = ?", id).First(existing).Error; err != nil {
  308. return err
  309. }
  310. updates := map[string]any{
  311. "name": in.Name,
  312. "remark": in.Remark,
  313. "scheme": in.Scheme,
  314. "address": in.Address,
  315. "port": in.Port,
  316. "base_path": in.BasePath,
  317. "api_token": in.ApiToken,
  318. "enable": in.Enable,
  319. "allow_private_address": in.AllowPrivateAddress,
  320. "tls_verify_mode": in.TlsVerifyMode,
  321. "pinned_cert_sha256": in.PinnedCertSha256,
  322. "inbound_sync_mode": in.InboundSyncMode,
  323. "inbound_tags": string(inboundTagsJSON),
  324. }
  325. if err := db.Model(model.Node{}).Where("id = ?", id).Updates(updates).Error; err != nil {
  326. return err
  327. }
  328. if mgr := runtime.GetManager(); mgr != nil {
  329. mgr.InvalidateNode(id)
  330. }
  331. return nil
  332. }
  333. func (s *NodeService) GetRemoteInboundOptions(ctx context.Context, n *model.Node) ([]runtime.RemoteInboundOption, error) {
  334. if err := s.normalize(n); err != nil {
  335. return nil, err
  336. }
  337. return runtime.NewRemote(n).ListInboundOptions(ctx)
  338. }
  339. // EnsureInboundTagAllowed adds a panel-managed inbound's tag to the node's
  340. // selection when the node syncs in "selected" mode. Without it, the next
  341. // traffic sync would filter the tag out of the snapshot and the orphan sweep
  342. // would silently delete the central row the panel just created or renamed.
  343. // Tags are only ever added (never removed): on a rename the node may keep
  344. // reporting the old tag until the remote update lands, and a leftover entry
  345. // that matches nothing is harmless.
  346. func (s *NodeService) EnsureInboundTagAllowed(nodeID int, tag string) error {
  347. tag = strings.TrimSpace(tag)
  348. if nodeID <= 0 || tag == "" {
  349. return nil
  350. }
  351. db := database.GetDB()
  352. node := &model.Node{}
  353. if err := db.Where("id = ?", nodeID).First(node).Error; err != nil {
  354. return err
  355. }
  356. if node.InboundSyncMode != "selected" {
  357. return nil
  358. }
  359. for _, t := range node.InboundTags {
  360. if t == tag {
  361. return nil
  362. }
  363. }
  364. buf, err := json.Marshal(append(node.InboundTags, tag))
  365. if err != nil {
  366. return err
  367. }
  368. return db.Model(model.Node{}).Where("id = ?", nodeID).
  369. Updates(map[string]any{"inbound_tags": string(buf)}).Error
  370. }
  371. func FilterNodeSnapshot(n *model.Node, snap *runtime.TrafficSnapshot) {
  372. if n == nil || snap == nil || n.InboundSyncMode != "selected" {
  373. return
  374. }
  375. allowed := make(map[string]struct{}, len(n.InboundTags))
  376. for _, tag := range n.InboundTags {
  377. allowed[tag] = struct{}{}
  378. }
  379. filtered := make([]*model.Inbound, 0, len(snap.Inbounds))
  380. for _, inbound := range snap.Inbounds {
  381. if inbound == nil {
  382. continue
  383. }
  384. if _, ok := allowed[inbound.Tag]; ok {
  385. filtered = append(filtered, inbound)
  386. }
  387. }
  388. snap.Inbounds = filtered
  389. }
  390. func (s *NodeService) Delete(id int) error {
  391. db := database.GetDB()
  392. if err := db.Where("id = ?", id).Delete(model.Node{}).Error; err != nil {
  393. return err
  394. }
  395. if err := db.Where("node_id = ?", id).Delete(&model.NodeClientTraffic{}).Error; err != nil {
  396. return err
  397. }
  398. if mgr := runtime.GetManager(); mgr != nil {
  399. mgr.InvalidateNode(id)
  400. }
  401. nodeMetrics.drop(nodeMetricKey(id, "cpu"))
  402. nodeMetrics.drop(nodeMetricKey(id, "mem"))
  403. return nil
  404. }
  405. func (s *NodeService) SetEnable(id int, enable bool) error {
  406. db := database.GetDB()
  407. return db.Model(model.Node{}).Where("id = ?", id).Update("enable", enable).Error
  408. }
  409. // GetWebCertFiles asks a node for its own web TLS certificate/key file paths,
  410. // used by "Set Cert from Panel" so a node-assigned inbound gets paths that
  411. // exist on the node rather than the central panel. See issue #4854.
  412. func (s *NodeService) GetWebCertFiles(id int) (*runtime.WebCertFiles, error) {
  413. n, err := s.GetById(id)
  414. if err != nil || n == nil {
  415. return nil, fmt.Errorf("node not found")
  416. }
  417. if !n.Enable {
  418. return nil, fmt.Errorf("node is disabled")
  419. }
  420. mgr := runtime.GetManager()
  421. if mgr == nil {
  422. return nil, fmt.Errorf("runtime manager unavailable")
  423. }
  424. remote, err := mgr.RemoteFor(n)
  425. if err != nil {
  426. return nil, err
  427. }
  428. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  429. defer cancel()
  430. return remote.GetWebCertFiles(ctx)
  431. }
  432. // NodeUpdateResult reports the outcome of triggering a panel self-update on one
  433. // node so the UI can show per-node success/failure for a bulk request.
  434. type NodeUpdateResult struct {
  435. Id int `json:"id"`
  436. Name string `json:"name"`
  437. OK bool `json:"ok"`
  438. Error string `json:"error,omitempty"`
  439. }
  440. // UpdatePanels triggers the official self-updater on each given node. Only
  441. // enabled, online nodes are eligible — an offline node can't be reached, so it
  442. // is reported as skipped rather than silently dropped.
  443. func (s *NodeService) UpdatePanels(ids []int) ([]NodeUpdateResult, error) {
  444. mgr := runtime.GetManager()
  445. if mgr == nil {
  446. return nil, fmt.Errorf("runtime manager unavailable")
  447. }
  448. results := make([]NodeUpdateResult, 0, len(ids))
  449. for _, id := range ids {
  450. n, err := s.GetById(id)
  451. if err != nil || n == nil {
  452. results = append(results, NodeUpdateResult{Id: id, OK: false, Error: "node not found"})
  453. continue
  454. }
  455. res := NodeUpdateResult{Id: id, Name: n.Name}
  456. switch {
  457. case !n.Enable:
  458. res.Error = "node is disabled"
  459. case n.Status != "online":
  460. res.Error = "node is offline"
  461. default:
  462. remote, remoteErr := mgr.RemoteFor(n)
  463. if remoteErr != nil {
  464. res.Error = remoteErr.Error()
  465. break
  466. }
  467. ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
  468. updErr := remote.UpdatePanel(ctx)
  469. cancel()
  470. if updErr != nil {
  471. res.Error = updErr.Error()
  472. } else {
  473. res.OK = true
  474. }
  475. }
  476. results = append(results, res)
  477. }
  478. return results, nil
  479. }
  480. func (s *NodeService) UpdateHeartbeat(id int, p HeartbeatPatch) error {
  481. db := database.GetDB()
  482. updates := map[string]any{
  483. "status": p.Status,
  484. "last_heartbeat": p.LastHeartbeat,
  485. "latency_ms": p.LatencyMs,
  486. "xray_version": p.XrayVersion,
  487. "panel_version": p.PanelVersion,
  488. "cpu_pct": p.CpuPct,
  489. "mem_pct": p.MemPct,
  490. "uptime_secs": p.UptimeSecs,
  491. "last_error": p.LastError,
  492. "xray_state": p.XrayState,
  493. "xray_error": p.XrayError,
  494. }
  495. // Only learn the GUID; never clear a known one if an old-build node (or a
  496. // failed probe) reports none, so the stable identity survives blips.
  497. if p.Guid != "" {
  498. updates["guid"] = p.Guid
  499. }
  500. if err := db.Model(model.Node{}).Where("id = ?", id).Updates(updates).Error; err != nil {
  501. return err
  502. }
  503. if p.Status == "online" {
  504. now := time.Unix(p.LastHeartbeat, 0)
  505. nodeMetrics.append(nodeMetricKey(id, "cpu"), now, p.CpuPct)
  506. nodeMetrics.append(nodeMetricKey(id, "mem"), now, p.MemPct)
  507. }
  508. return nil
  509. }
  510. func (s *NodeService) MarkNodeDirty(id int) error {
  511. if id <= 0 {
  512. return nil
  513. }
  514. return database.GetDB().Model(model.Node{}).
  515. Where("id = ?", id).
  516. Updates(map[string]any{
  517. "config_dirty": true,
  518. "config_dirty_at": time.Now().UnixMilli(),
  519. }).Error
  520. }
  521. func (s *NodeService) ClearNodeDirty(id int, dirtyAt int64) error {
  522. if id <= 0 {
  523. return nil
  524. }
  525. return database.GetDB().Model(model.Node{}).
  526. Where("id = ? AND config_dirty_at = ?", id, dirtyAt).
  527. Update("config_dirty", false).Error
  528. }
  529. func (s *NodeService) NodeSyncState(id int) (enabled bool, status string, dirty bool, dirtyAt int64, err error) {
  530. if id <= 0 {
  531. return false, "", false, 0, errors.New("invalid node id")
  532. }
  533. var row model.Node
  534. err = database.GetDB().Model(model.Node{}).
  535. Select("enable", "status", "config_dirty", "config_dirty_at").
  536. Where("id = ?", id).
  537. First(&row).Error
  538. if err != nil {
  539. return false, "", false, 0, err
  540. }
  541. return row.Enable, row.Status, row.ConfigDirty, row.ConfigDirtyAt, nil
  542. }
  543. func (s *NodeService) IsNodePending(id int) bool {
  544. enabled, status, dirty, _, err := s.NodeSyncState(id)
  545. if err != nil {
  546. return false
  547. }
  548. return !enabled || status != "online" || dirty
  549. }
  550. func nodeMetricKey(id int, metric string) string {
  551. return "node:" + strconv.Itoa(id) + ":" + metric
  552. }
  553. func (s *NodeService) AggregateNodeMetric(id int, metric string, bucketSeconds int, maxPoints int) []map[string]any {
  554. return nodeMetrics.aggregate(nodeMetricKey(id, metric), bucketSeconds, maxPoints)
  555. }
  556. func (s *NodeService) Probe(ctx context.Context, n *model.Node) (HeartbeatPatch, error) {
  557. patch := HeartbeatPatch{LastHeartbeat: time.Now().Unix()}
  558. addr, err := netsafe.NormalizeHost(n.Address)
  559. if err != nil {
  560. patch.LastError = err.Error()
  561. return patch, err
  562. }
  563. scheme := n.Scheme
  564. if scheme != "http" && scheme != "https" {
  565. scheme = "https"
  566. }
  567. if n.Port <= 0 || n.Port > 65535 {
  568. patch.LastError = "node port must be 1-65535"
  569. return patch, errors.New(patch.LastError)
  570. }
  571. probeURL := &url.URL{
  572. Scheme: scheme,
  573. Host: net.JoinHostPort(addr, strconv.Itoa(n.Port)),
  574. Path: normalizeBasePath(n.BasePath) + "panel/api/server/status",
  575. }
  576. req, err := http.NewRequestWithContext(
  577. netsafe.ContextWithAllowPrivate(ctx, n.AllowPrivateAddress),
  578. http.MethodGet, probeURL.String(), nil)
  579. if err != nil {
  580. patch.LastError = err.Error()
  581. return patch, err
  582. }
  583. if n.ApiToken != "" {
  584. req.Header.Set("Authorization", "Bearer "+n.ApiToken)
  585. }
  586. req.Header.Set("Accept", "application/json")
  587. client, err := runtime.HTTPClientForNode(n)
  588. if err != nil {
  589. patch.LastError = err.Error()
  590. return patch, err
  591. }
  592. start := time.Now()
  593. resp, err := client.Do(req)
  594. if err != nil {
  595. patch.LastError = err.Error()
  596. return patch, err
  597. }
  598. defer resp.Body.Close()
  599. patch.LatencyMs = int(time.Since(start) / time.Millisecond)
  600. if resp.StatusCode != http.StatusOK {
  601. patch.LastError = fmt.Sprintf("HTTP %d from remote panel", resp.StatusCode)
  602. return patch, errors.New(patch.LastError)
  603. }
  604. var envelope struct {
  605. Success bool `json:"success"`
  606. Msg string `json:"msg"`
  607. Obj *struct {
  608. CpuPct float64 `json:"cpu"`
  609. Mem struct {
  610. Current uint64 `json:"current"`
  611. Total uint64 `json:"total"`
  612. } `json:"mem"`
  613. Xray struct {
  614. Version string `json:"version"`
  615. State string `json:"state"`
  616. ErrorMsg string `json:"errorMsg"`
  617. } `json:"xray"`
  618. PanelVersion string `json:"panelVersion"`
  619. PanelGuid string `json:"panelGuid"`
  620. Uptime uint64 `json:"uptime"`
  621. } `json:"obj"`
  622. }
  623. if err := json.NewDecoder(resp.Body).Decode(&envelope); err != nil {
  624. patch.LastError = "decode response: " + err.Error()
  625. return patch, err
  626. }
  627. if !envelope.Success || envelope.Obj == nil {
  628. patch.LastError = "remote returned success=false: " + envelope.Msg
  629. return patch, errors.New(patch.LastError)
  630. }
  631. o := envelope.Obj
  632. patch.CpuPct = o.CpuPct
  633. if o.Mem.Total > 0 {
  634. patch.MemPct = float64(o.Mem.Current) * 100.0 / float64(o.Mem.Total)
  635. }
  636. patch.XrayVersion = o.Xray.Version
  637. patch.XrayState = o.Xray.State
  638. patch.XrayError = o.Xray.ErrorMsg
  639. patch.PanelVersion = o.PanelVersion
  640. patch.Guid = o.PanelGuid
  641. patch.UptimeSecs = o.Uptime
  642. return patch, nil
  643. }
  644. type ProbeResultUI struct {
  645. Status string `json:"status" example:"online"`
  646. LatencyMs int `json:"latencyMs" example:"42"`
  647. XrayVersion string `json:"xrayVersion" example:"25.10.31"`
  648. PanelVersion string `json:"panelVersion" example:"v3.x.x"`
  649. CpuPct float64 `json:"cpuPct" example:"12.5"`
  650. MemPct float64 `json:"memPct" example:"45.2"`
  651. UptimeSecs uint64 `json:"uptimeSecs" example:"86400"`
  652. Error string `json:"error"`
  653. // XrayState/XrayError are populated on successful probes even when the node's
  654. // Xray core is not healthy. The UI uses them for a distinct "panel ok, xray failed" indicator.
  655. XrayState string `json:"xrayState"`
  656. XrayError string `json:"xrayError"`
  657. }
  658. func (p HeartbeatPatch) ToUI(ok bool) ProbeResultUI {
  659. r := ProbeResultUI{
  660. LatencyMs: p.LatencyMs,
  661. XrayVersion: p.XrayVersion,
  662. PanelVersion: p.PanelVersion,
  663. CpuPct: p.CpuPct,
  664. MemPct: p.MemPct,
  665. UptimeSecs: p.UptimeSecs,
  666. Error: FriendlyProbeError(p.LastError),
  667. XrayState: p.XrayState,
  668. XrayError: p.XrayError,
  669. }
  670. if ok {
  671. r.Status = "online"
  672. } else {
  673. r.Status = "offline"
  674. }
  675. return r
  676. }
  677. func FriendlyProbeError(msg string) string {
  678. if strings.Contains(msg, "server gave HTTP response to HTTPS client") {
  679. return "the server speaks HTTP, not HTTPS; set the node scheme to http"
  680. }
  681. return msg
  682. }