1
0

node.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. package controller
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "slices"
  7. "strconv"
  8. "time"
  9. "github.com/mhsanaei/3x-ui/v3/database/model"
  10. "github.com/mhsanaei/3x-ui/v3/web/middleware"
  11. "github.com/mhsanaei/3x-ui/v3/web/service"
  12. "github.com/gin-gonic/gin"
  13. )
  14. type NodeController struct {
  15. nodeService service.NodeService
  16. }
  17. func NewNodeController(g *gin.RouterGroup) *NodeController {
  18. a := &NodeController{}
  19. a.initRouter(g)
  20. return a
  21. }
  22. func (a *NodeController) initRouter(g *gin.RouterGroup) {
  23. g.GET("/list", a.list)
  24. g.GET("/get/:id", a.get)
  25. g.GET("/webCert/:id", a.webCert)
  26. g.POST("/add", a.add)
  27. g.POST("/update/:id", a.update)
  28. g.POST("/del/:id", a.del)
  29. g.POST("/setEnable/:id", a.setEnable)
  30. g.POST("/test", a.test)
  31. g.POST("/certFingerprint", a.certFingerprint)
  32. g.POST("/probe/:id", a.probe)
  33. g.POST("/updatePanel", a.updatePanel)
  34. g.GET("/history/:id/:metric/:bucket", a.history)
  35. }
  36. func (a *NodeController) list(c *gin.Context) {
  37. nodes, err := a.nodeService.GetAll()
  38. if err != nil {
  39. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.list"), err)
  40. return
  41. }
  42. jsonObj(c, nodes, nil)
  43. }
  44. func (a *NodeController) get(c *gin.Context) {
  45. id, err := strconv.Atoi(c.Param("id"))
  46. if err != nil {
  47. jsonMsg(c, I18nWeb(c, "get"), err)
  48. return
  49. }
  50. n, err := a.nodeService.GetById(id)
  51. if err != nil {
  52. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.obtain"), err)
  53. return
  54. }
  55. jsonObj(c, n, nil)
  56. }
  57. // webCert returns the node's own web TLS certificate/key file paths so the
  58. // inbound form's "Set Cert from Panel" can fill paths that exist on the node.
  59. func (a *NodeController) webCert(c *gin.Context) {
  60. id, err := strconv.Atoi(c.Param("id"))
  61. if err != nil {
  62. jsonMsg(c, I18nWeb(c, "get"), err)
  63. return
  64. }
  65. files, err := a.nodeService.GetWebCertFiles(id)
  66. if err != nil {
  67. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.obtain"), err)
  68. return
  69. }
  70. jsonObj(c, files, nil)
  71. }
  72. func (a *NodeController) ensureReachable(c *gin.Context, n *model.Node) error {
  73. ctx, cancel := context.WithTimeout(c.Request.Context(), 6*time.Second)
  74. defer cancel()
  75. if _, err := a.nodeService.Probe(ctx, n); err != nil {
  76. return errors.New(service.FriendlyProbeError(err.Error()))
  77. }
  78. return nil
  79. }
  80. func (a *NodeController) add(c *gin.Context) {
  81. n, ok := middleware.BindAndValidate[model.Node](c)
  82. if !ok {
  83. return
  84. }
  85. if err := a.ensureReachable(c, n); err != nil {
  86. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.add"), err)
  87. return
  88. }
  89. if err := a.nodeService.Create(n); err != nil {
  90. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.add"), err)
  91. return
  92. }
  93. jsonMsgObj(c, I18nWeb(c, "pages.nodes.toasts.add"), n, nil)
  94. }
  95. func (a *NodeController) update(c *gin.Context) {
  96. id, err := strconv.Atoi(c.Param("id"))
  97. if err != nil {
  98. jsonMsg(c, I18nWeb(c, "get"), err)
  99. return
  100. }
  101. n, ok := middleware.BindAndValidate[model.Node](c)
  102. if !ok {
  103. return
  104. }
  105. if err := a.ensureReachable(c, n); err != nil {
  106. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.update"), err)
  107. return
  108. }
  109. if err := a.nodeService.Update(id, n); err != nil {
  110. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.update"), err)
  111. return
  112. }
  113. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.update"), nil)
  114. }
  115. func (a *NodeController) del(c *gin.Context) {
  116. id, err := strconv.Atoi(c.Param("id"))
  117. if err != nil {
  118. jsonMsg(c, I18nWeb(c, "get"), err)
  119. return
  120. }
  121. if err := a.nodeService.Delete(id); err != nil {
  122. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.delete"), err)
  123. return
  124. }
  125. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.delete"), nil)
  126. }
  127. func (a *NodeController) setEnable(c *gin.Context) {
  128. id, err := strconv.Atoi(c.Param("id"))
  129. if err != nil {
  130. jsonMsg(c, I18nWeb(c, "get"), err)
  131. return
  132. }
  133. body := struct {
  134. Enable bool `json:"enable" form:"enable"`
  135. }{}
  136. if err := c.ShouldBind(&body); err != nil {
  137. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.update"), err)
  138. return
  139. }
  140. if err := a.nodeService.SetEnable(id, body.Enable); err != nil {
  141. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.update"), err)
  142. return
  143. }
  144. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.update"), nil)
  145. }
  146. func (a *NodeController) test(c *gin.Context) {
  147. n := &model.Node{}
  148. if err := c.ShouldBind(n); err != nil {
  149. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.test"), err)
  150. return
  151. }
  152. if n.Scheme == "" {
  153. n.Scheme = "https"
  154. }
  155. if n.BasePath == "" {
  156. n.BasePath = "/"
  157. }
  158. ctx, cancel := context.WithTimeout(c.Request.Context(), 6*time.Second)
  159. defer cancel()
  160. patch, err := a.nodeService.Probe(ctx, n)
  161. jsonObj(c, patch.ToUI(err == nil), nil)
  162. }
  163. func (a *NodeController) certFingerprint(c *gin.Context) {
  164. n := &model.Node{}
  165. if err := c.ShouldBind(n); err != nil {
  166. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.test"), err)
  167. return
  168. }
  169. if n.Scheme == "" {
  170. n.Scheme = "https"
  171. }
  172. if n.BasePath == "" {
  173. n.BasePath = "/"
  174. }
  175. ctx, cancel := context.WithTimeout(c.Request.Context(), 6*time.Second)
  176. defer cancel()
  177. fp, err := a.nodeService.FetchCertFingerprint(ctx, n)
  178. if err != nil {
  179. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.test"), err)
  180. return
  181. }
  182. jsonObj(c, fp, nil)
  183. }
  184. func (a *NodeController) probe(c *gin.Context) {
  185. id, err := strconv.Atoi(c.Param("id"))
  186. if err != nil {
  187. jsonMsg(c, I18nWeb(c, "get"), err)
  188. return
  189. }
  190. n, err := a.nodeService.GetById(id)
  191. if err != nil {
  192. jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.obtain"), err)
  193. return
  194. }
  195. ctx, cancel := context.WithTimeout(c.Request.Context(), 6*time.Second)
  196. defer cancel()
  197. patch, probeErr := a.nodeService.Probe(ctx, n)
  198. if probeErr != nil {
  199. patch.Status = "offline"
  200. } else {
  201. patch.Status = "online"
  202. }
  203. _ = a.nodeService.UpdateHeartbeat(id, patch)
  204. jsonObj(c, patch.ToUI(probeErr == nil), nil)
  205. }
  206. func (a *NodeController) updatePanel(c *gin.Context) {
  207. var req struct {
  208. Ids []int `json:"ids"`
  209. }
  210. if err := c.ShouldBindJSON(&req); err != nil {
  211. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  212. return
  213. }
  214. if len(req.Ids) == 0 {
  215. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), fmt.Errorf("no nodes selected"))
  216. return
  217. }
  218. results, err := a.nodeService.UpdatePanels(req.Ids)
  219. jsonMsgObj(c, I18nWeb(c, "pages.nodes.toasts.updateStarted"), results, err)
  220. }
  221. func (a *NodeController) history(c *gin.Context) {
  222. id, err := strconv.Atoi(c.Param("id"))
  223. if err != nil {
  224. jsonMsg(c, I18nWeb(c, "get"), err)
  225. return
  226. }
  227. metric := c.Param("metric")
  228. if !slices.Contains(service.NodeMetricKeys, metric) {
  229. jsonMsg(c, "invalid metric", fmt.Errorf("unknown metric"))
  230. return
  231. }
  232. bucket, err := strconv.Atoi(c.Param("bucket"))
  233. if err != nil || bucket <= 0 || !service.IsAllowedHistoryBucket(bucket) {
  234. jsonMsg(c, "invalid bucket", fmt.Errorf("unsupported bucket"))
  235. return
  236. }
  237. jsonObj(c, a.nodeService.AggregateNodeMetric(id, metric, bucket, 60), nil)
  238. }