1
0

node.go 8.2 KB

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