inbound.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. package controller
  2. import (
  3. "encoding/json"
  4. "net"
  5. "strconv"
  6. "strings"
  7. "github.com/mhsanaei/3x-ui/v3/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/web/middleware"
  9. "github.com/mhsanaei/3x-ui/v3/web/service"
  10. "github.com/mhsanaei/3x-ui/v3/web/session"
  11. "github.com/mhsanaei/3x-ui/v3/web/websocket"
  12. "github.com/gin-gonic/gin"
  13. )
  14. // InboundController handles HTTP requests related to Xray inbounds management.
  15. type InboundController struct {
  16. inboundService service.InboundService
  17. clientService service.ClientService
  18. xrayService service.XrayService
  19. fallbackService service.FallbackService
  20. }
  21. // NewInboundController creates a new InboundController and sets up its routes.
  22. func NewInboundController(g *gin.RouterGroup) *InboundController {
  23. a := &InboundController{}
  24. a.initRouter(g)
  25. return a
  26. }
  27. // broadcastInboundsUpdateClientLimit is the threshold past which we skip the
  28. // full-list push over WebSocket and signal the frontend to re-fetch via REST.
  29. // Mirrors the same heuristic used by the periodic traffic job.
  30. const broadcastInboundsUpdateClientLimit = 5000
  31. // broadcastInboundsUpdate fetches and broadcasts the inbound list for userId.
  32. // At scale (10k+ clients) the marshaled JSON exceeds the WS payload ceiling,
  33. // so we send an invalidate signal instead — frontend re-fetches via REST.
  34. // Skipped entirely when no WebSocket clients are connected.
  35. func (a *InboundController) broadcastInboundsUpdate(userId int) {
  36. if !websocket.HasClients() {
  37. return
  38. }
  39. inbounds, err := a.inboundService.GetInbounds(userId)
  40. if err != nil {
  41. return
  42. }
  43. totalClients := 0
  44. for _, ib := range inbounds {
  45. totalClients += len(ib.ClientStats)
  46. }
  47. if totalClients > broadcastInboundsUpdateClientLimit {
  48. websocket.BroadcastInvalidate(websocket.MessageTypeInbounds)
  49. return
  50. }
  51. websocket.BroadcastInbounds(inbounds)
  52. }
  53. // initRouter initializes the routes for inbound-related operations.
  54. func (a *InboundController) initRouter(g *gin.RouterGroup) {
  55. g.GET("/list", a.getInbounds)
  56. g.GET("/list/slim", a.getInboundsSlim)
  57. g.GET("/options", a.getInboundOptions)
  58. g.GET("/get/:id", a.getInbound)
  59. g.GET("/:id/fallbacks", a.getFallbacks)
  60. g.POST("/add", a.addInbound)
  61. g.POST("/del/:id", a.delInbound)
  62. g.POST("/update/:id", a.updateInbound)
  63. g.POST("/setEnable/:id", a.setInboundEnable)
  64. g.POST("/:id/resetTraffic", a.resetInboundTraffic)
  65. g.POST("/:id/delAllClients", a.delAllInboundClients)
  66. g.POST("/resetAllTraffics", a.resetAllTraffics)
  67. g.POST("/import", a.importInbound)
  68. g.POST("/:id/fallbacks", a.setFallbacks)
  69. }
  70. // getInbounds retrieves the list of inbounds for the logged-in user.
  71. func (a *InboundController) getInbounds(c *gin.Context) {
  72. user := session.GetLoginUser(c)
  73. inbounds, err := a.inboundService.GetInbounds(user.Id)
  74. if err != nil {
  75. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
  76. return
  77. }
  78. jsonObj(c, inbounds, nil)
  79. }
  80. // getInboundsSlim is the list-page variant that strips full client
  81. // payloads from settings.clients[]. Detail-view flows still use /get/:id.
  82. func (a *InboundController) getInboundsSlim(c *gin.Context) {
  83. user := session.GetLoginUser(c)
  84. inbounds, err := a.inboundService.GetInboundsSlim(user.Id)
  85. if err != nil {
  86. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
  87. return
  88. }
  89. jsonObj(c, inbounds, nil)
  90. }
  91. // getInboundOptions returns a lightweight projection of the user's inbounds
  92. // (id, remark, protocol, port, tlsFlowCapable) for pickers in the clients UI.
  93. // Avoids shipping per-client settings and traffic stats just to fill a dropdown.
  94. func (a *InboundController) getInboundOptions(c *gin.Context) {
  95. user := session.GetLoginUser(c)
  96. options, err := a.inboundService.GetInboundOptions(user.Id)
  97. if err != nil {
  98. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
  99. return
  100. }
  101. jsonObj(c, options, nil)
  102. }
  103. // getInbound retrieves a specific inbound by its ID.
  104. func (a *InboundController) getInbound(c *gin.Context) {
  105. id, err := strconv.Atoi(c.Param("id"))
  106. if err != nil {
  107. jsonMsg(c, I18nWeb(c, "get"), err)
  108. return
  109. }
  110. inbound, err := a.inboundService.GetInbound(id)
  111. if err != nil {
  112. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
  113. return
  114. }
  115. jsonObj(c, inbound, nil)
  116. }
  117. // addInbound creates a new inbound configuration.
  118. func (a *InboundController) addInbound(c *gin.Context) {
  119. inbound, ok := middleware.BindAndValidate[model.Inbound](c)
  120. if !ok {
  121. return
  122. }
  123. user := session.GetLoginUser(c)
  124. inbound.UserId = user.Id
  125. // Treat NodeID=0 as "no node" — gin's *int form binding can land on
  126. // 0 when the field is absent or empty, and 0 is never a valid Node
  127. // row id. Without this normalization the runtime layer would try to
  128. // load Node id=0 and surface "record not found".
  129. if inbound.NodeID != nil && *inbound.NodeID == 0 {
  130. inbound.NodeID = nil
  131. }
  132. inbound, needRestart, err := a.inboundService.AddInbound(inbound)
  133. if err != nil {
  134. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  135. return
  136. }
  137. jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, nil)
  138. if needRestart {
  139. a.xrayService.SetToNeedRestart()
  140. }
  141. a.broadcastInboundsUpdate(user.Id)
  142. notifyClientsChanged()
  143. }
  144. // delInbound deletes an inbound configuration by its ID.
  145. func (a *InboundController) delInbound(c *gin.Context) {
  146. id, err := strconv.Atoi(c.Param("id"))
  147. if err != nil {
  148. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), err)
  149. return
  150. }
  151. needRestart, err := a.inboundService.DelInbound(id)
  152. if err != nil {
  153. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  154. return
  155. }
  156. jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, nil)
  157. if needRestart {
  158. a.xrayService.SetToNeedRestart()
  159. }
  160. user := session.GetLoginUser(c)
  161. a.broadcastInboundsUpdate(user.Id)
  162. notifyClientsChanged()
  163. }
  164. // updateInbound updates an existing inbound configuration.
  165. func (a *InboundController) updateInbound(c *gin.Context) {
  166. id, err := strconv.Atoi(c.Param("id"))
  167. if err != nil {
  168. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
  169. return
  170. }
  171. inbound := &model.Inbound{
  172. Id: id,
  173. }
  174. if !middleware.BindAndValidateInto(c, inbound) {
  175. return
  176. }
  177. // Same NodeID=0 → nil normalisation as addInbound. UpdateInbound
  178. // loads the existing row's NodeID from DB anyway (Phase 1 doesn't
  179. // support migrating an inbound between nodes), but normalising here
  180. // keeps the wire shape consistent.
  181. if inbound.NodeID != nil && *inbound.NodeID == 0 {
  182. inbound.NodeID = nil
  183. }
  184. inbound, needRestart, err := a.inboundService.UpdateInbound(inbound)
  185. if err != nil {
  186. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  187. return
  188. }
  189. jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, nil)
  190. if needRestart {
  191. a.xrayService.SetToNeedRestart()
  192. }
  193. user := session.GetLoginUser(c)
  194. a.broadcastInboundsUpdate(user.Id)
  195. notifyClientsChanged()
  196. }
  197. // setInboundEnable flips only the enable flag of an inbound. This is a
  198. // dedicated endpoint because the regular update path serialises the entire
  199. // settings JSON (every client) — far too heavy for an interactive switch
  200. // on inbounds with thousands of clients. Frontend optimistically updates
  201. // the UI; we just persist + sync xray + nudge other open admin sessions.
  202. func (a *InboundController) setInboundEnable(c *gin.Context) {
  203. id, err := strconv.Atoi(c.Param("id"))
  204. if err != nil {
  205. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
  206. return
  207. }
  208. type form struct {
  209. Enable bool `json:"enable" form:"enable"`
  210. }
  211. var f form
  212. if err := c.ShouldBind(&f); err != nil {
  213. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  214. return
  215. }
  216. needRestart, err := a.inboundService.SetInboundEnable(id, f.Enable)
  217. if err != nil {
  218. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  219. return
  220. }
  221. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), nil)
  222. if needRestart {
  223. a.xrayService.SetToNeedRestart()
  224. }
  225. // Cross-admin sync: lightweight invalidate signal (a few hundred bytes)
  226. // instead of fetching + serialising the whole inbound list. Other open
  227. // sessions re-fetch via REST. The toggling admin's own UI already
  228. // updated optimistically.
  229. websocket.BroadcastInvalidate(websocket.MessageTypeInbounds)
  230. }
  231. // resetInboundTraffic resets traffic counters for a specific inbound.
  232. func (a *InboundController) resetInboundTraffic(c *gin.Context) {
  233. id, err := strconv.Atoi(c.Param("id"))
  234. if err != nil {
  235. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
  236. return
  237. }
  238. err = a.inboundService.ResetInboundTraffic(id)
  239. if err != nil {
  240. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  241. return
  242. } else {
  243. a.xrayService.SetToNeedRestart()
  244. }
  245. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetInboundTrafficSuccess"), nil)
  246. }
  247. // delAllInboundClients removes every client attached to a specific inbound
  248. // while keeping the inbound itself. Internally collects the current email
  249. // list from settings.clients[] and feeds it into ClientService.BulkDelete,
  250. // which handles per-inbound JSON rewriting, runtime user removal, traffic
  251. // row cleanup, and the SyncInbound mapping pass in one optimized cycle.
  252. func (a *InboundController) delAllInboundClients(c *gin.Context) {
  253. id, err := strconv.Atoi(c.Param("id"))
  254. if err != nil {
  255. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  256. return
  257. }
  258. emails, err := a.inboundService.EmailsByInbound(id)
  259. if err != nil {
  260. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  261. return
  262. }
  263. if len(emails) == 0 {
  264. jsonObj(c, service.BulkDeleteResult{}, nil)
  265. return
  266. }
  267. result, needRestart, err := a.clientService.BulkDelete(&a.inboundService, emails, false)
  268. if err != nil {
  269. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  270. return
  271. }
  272. jsonObj(c, result, nil)
  273. if needRestart {
  274. a.xrayService.SetToNeedRestart()
  275. }
  276. user := session.GetLoginUser(c)
  277. a.broadcastInboundsUpdate(user.Id)
  278. notifyClientsChanged()
  279. }
  280. // resetAllTraffics resets all traffic counters across all inbounds.
  281. func (a *InboundController) resetAllTraffics(c *gin.Context) {
  282. err := a.inboundService.ResetAllTraffics()
  283. if err != nil {
  284. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  285. return
  286. } else {
  287. a.xrayService.SetToNeedRestart()
  288. }
  289. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllTrafficSuccess"), nil)
  290. }
  291. // importInbound imports an inbound configuration from provided data.
  292. func (a *InboundController) importInbound(c *gin.Context) {
  293. inbound := &model.Inbound{}
  294. err := json.Unmarshal([]byte(c.PostForm("data")), inbound)
  295. if err != nil {
  296. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  297. return
  298. }
  299. user := session.GetLoginUser(c)
  300. inbound.Id = 0
  301. inbound.UserId = user.Id
  302. if inbound.NodeID != nil && *inbound.NodeID == 0 {
  303. inbound.NodeID = nil
  304. }
  305. for index := range inbound.ClientStats {
  306. inbound.ClientStats[index].Id = 0
  307. inbound.ClientStats[index].Enable = true
  308. }
  309. inbound, needRestart, err := a.inboundService.AddInbound(inbound)
  310. if err != nil {
  311. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  312. return
  313. }
  314. jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, nil)
  315. if needRestart {
  316. a.xrayService.SetToNeedRestart()
  317. }
  318. a.broadcastInboundsUpdate(user.Id)
  319. notifyClientsChanged()
  320. }
  321. // resolveHost mirrors what sub.SubService.ResolveRequest does for the host
  322. // field: prefers X-Forwarded-Host (first entry of any list, port stripped),
  323. // then X-Real-IP, then the host portion of c.Request.Host. Keeping it in the
  324. // controller layer means the service interface stays HTTP-agnostic — service
  325. // methods receive a plain host string instead of a *gin.Context.
  326. func resolveHost(c *gin.Context) string {
  327. if isTrustedForwardedRequest(c) {
  328. if h := strings.TrimSpace(c.GetHeader("X-Forwarded-Host")); h != "" {
  329. if i := strings.Index(h, ","); i >= 0 {
  330. h = strings.TrimSpace(h[:i])
  331. }
  332. if hp, _, err := net.SplitHostPort(h); err == nil {
  333. return hp
  334. }
  335. return h
  336. }
  337. if h := c.GetHeader("X-Real-IP"); h != "" {
  338. return h
  339. }
  340. }
  341. if h, _, err := net.SplitHostPort(c.Request.Host); err == nil {
  342. return h
  343. }
  344. return c.Request.Host
  345. }
  346. // getFallbacks returns the fallback rules attached to the master inbound.
  347. func (a *InboundController) getFallbacks(c *gin.Context) {
  348. id, err := strconv.Atoi(c.Param("id"))
  349. if err != nil {
  350. jsonMsg(c, I18nWeb(c, "get"), err)
  351. return
  352. }
  353. rows, err := a.fallbackService.GetByMaster(id)
  354. if err != nil {
  355. jsonMsg(c, I18nWeb(c, "get"), err)
  356. return
  357. }
  358. jsonObj(c, rows, nil)
  359. }
  360. // setFallbacks atomically replaces the master inbound's fallback list
  361. // and triggers an Xray restart so the new settings.fallbacks take effect.
  362. func (a *InboundController) setFallbacks(c *gin.Context) {
  363. id, err := strconv.Atoi(c.Param("id"))
  364. if err != nil {
  365. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  366. return
  367. }
  368. type body struct {
  369. Fallbacks []service.FallbackInput `json:"fallbacks"`
  370. }
  371. var b body
  372. if err := c.ShouldBindJSON(&b); err != nil {
  373. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  374. return
  375. }
  376. if err := a.fallbackService.SetByMaster(id, b.Fallbacks); err != nil {
  377. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  378. return
  379. }
  380. a.xrayService.SetToNeedRestart()
  381. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), nil)
  382. }