api.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. package controller
  2. import (
  3. "net/http"
  4. "strings"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  6. "github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
  7. "github.com/mhsanaei/3x-ui/v3/internal/web/service/panel"
  8. "github.com/mhsanaei/3x-ui/v3/internal/web/service/tgbot"
  9. "github.com/mhsanaei/3x-ui/v3/internal/web/session"
  10. "github.com/gin-gonic/gin"
  11. )
  12. // APIController handles the main API routes for the 3x-ui panel, including inbounds and server management.
  13. type APIController struct {
  14. BaseController
  15. inboundController *InboundController
  16. serverController *ServerController
  17. nodeController *NodeController
  18. hostController *HostController
  19. settingController *SettingController
  20. xraySettingController *XraySettingController
  21. userService panel.UserService
  22. apiTokenService panel.ApiTokenService
  23. Tgbot tgbot.Tgbot
  24. }
  25. // NewAPIController creates a new APIController instance and initializes its routes.
  26. func NewAPIController(g *gin.RouterGroup) *APIController {
  27. a := &APIController{}
  28. a.initRouter(g)
  29. return a
  30. }
  31. func (a *APIController) checkAPIAuth(c *gin.Context) {
  32. setAuthenticated := func(scope string) {
  33. if u, err := a.userService.GetFirstUser(); err == nil {
  34. session.SetAPIAuthUser(c, u)
  35. }
  36. c.Set("api_authed", true)
  37. c.Set("api_token_scope", scope)
  38. }
  39. matchedScope := ""
  40. if after, ok := strings.CutPrefix(c.GetHeader("Authorization"), "Bearer "); ok {
  41. if row, matched := a.apiTokenService.MatchToken(after); matched {
  42. matchedScope = row.Scope
  43. }
  44. }
  45. // Verified mTLS keeps its historical node-sync identity. Only an explicit
  46. // node-admin bearer elevates it to software-update authority.
  47. if c.Request.TLS != nil && len(c.Request.TLS.VerifiedChains) > 0 {
  48. scope := model.ApiScopeNodeSync
  49. if matchedScope == model.ApiScopeNodeAdmin {
  50. scope = model.ApiScopeNodeAdmin
  51. }
  52. setAuthenticated(scope)
  53. c.Next()
  54. return
  55. }
  56. if matchedScope != "" {
  57. setAuthenticated(matchedScope)
  58. c.Next()
  59. return
  60. }
  61. if !session.IsLogin(c) {
  62. // A presented Bearer token is not an anonymous scan: return 401 so
  63. // callers can distinguish a bad/disabled token from a wrong base path
  64. // (NoRoute still 404s). XHR keeps 401; bare unauthenticated stays 404.
  65. authHdr := c.GetHeader("Authorization")
  66. if strings.HasPrefix(authHdr, "Bearer ") || c.GetHeader("X-Requested-With") == "XMLHttpRequest" {
  67. c.AbortWithStatus(http.StatusUnauthorized)
  68. } else {
  69. c.AbortWithStatus(http.StatusNotFound)
  70. }
  71. return
  72. }
  73. c.Next()
  74. }
  75. // monitorScopeAllow exposes only status/metrics routes without sensitive data.
  76. // Keys are route patterns relative to /panel/api.
  77. var monitorScopeAllow = map[string]struct{}{
  78. "/server/status": {},
  79. "/server/cpuHistory/:bucket": {},
  80. "/server/history/:metric/:bucket": {},
  81. "/server/xrayMetricsState": {},
  82. "/server/xrayMetricsHistory/:metric/:bucket": {},
  83. "/server/xrayObservatory": {},
  84. "/server/xrayObservatoryHistory/:tag/:bucket": {},
  85. "/server/getXrayVersion": {},
  86. "/server/getPanelUpdateInfo": {},
  87. "/nodes/history/:id/:metric/:bucket": {},
  88. }
  89. // nodeSyncScopeAllow is the node-sync route/method allowlist relative to
  90. // /panel/api; Gin patterns prevent concrete parameters broadening authority.
  91. var nodeSyncScopeAllow = map[string]map[string]struct{}{
  92. "/server/status": {http.MethodGet: {}},
  93. "/inbounds/list": {http.MethodGet: {}},
  94. "/inbounds/add": {http.MethodPost: {}},
  95. "/inbounds/del/:id": {http.MethodPost: {}},
  96. "/inbounds/update/:id": {http.MethodPost: {}},
  97. "/inbounds/:id/subSortIndex": {http.MethodPost: {}},
  98. "/clients/add": {http.MethodPost: {}},
  99. "/clients/del/:email": {http.MethodPost: {}},
  100. "/clients/:email/detach": {http.MethodPost: {}},
  101. "/clients/update/:email": {http.MethodPost: {}},
  102. "/server/restartXrayService": {http.MethodPost: {}},
  103. "/server/getWebCertFiles": {http.MethodGet: {}},
  104. "/server/descendants": {http.MethodGet: {}},
  105. "/clients/resetTraffic/:email": {http.MethodPost: {}},
  106. "/clients/bulkResetTraffic": {http.MethodPost: {}},
  107. "/inbounds/resetAllTraffics": {http.MethodPost: {}},
  108. "/inbounds/:id/resetTraffic": {http.MethodPost: {}},
  109. "/clients/onlinesByGuid": {http.MethodPost: {}},
  110. "/clients/onlines": {http.MethodPost: {}},
  111. "/clients/lastOnline": {http.MethodPost: {}},
  112. "/clients/activeInbounds": {http.MethodPost: {}},
  113. "/inbounds/pushClientTraffics": {http.MethodPost: {}},
  114. "/server/clientIps": {http.MethodGet: {}, http.MethodPost: {}},
  115. "/clients/clientIpsByGuid": {http.MethodPost: {}},
  116. "/hosts/list": {http.MethodGet: {}},
  117. }
  118. var nodeAdminScopeAllow = map[string]map[string]struct{}{
  119. "/server/updatePanel": {http.MethodPost: {}},
  120. }
  121. func scopeAllows(allow map[string]map[string]struct{}, path, method string) bool {
  122. methods, exists := allow[path]
  123. if !exists {
  124. return false
  125. }
  126. _, exists = methods[method]
  127. return exists
  128. }
  129. // enforceTokenScope applies explicit allowlists to restricted API tokens.
  130. // Admin tokens and session-login users retain their existing behavior.
  131. func (a *APIController) enforceTokenScope(c *gin.Context) {
  132. scopeVal, ok := c.Get("api_token_scope")
  133. if !ok {
  134. c.Next()
  135. return
  136. }
  137. scope, _ := scopeVal.(string)
  138. if scope == model.ApiScopeAdmin {
  139. c.Next()
  140. return
  141. }
  142. deny := func() {
  143. c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
  144. "success": false,
  145. "msg": "this API token is not permitted to access this endpoint",
  146. })
  147. }
  148. rel := relAPIPath(c.FullPath())
  149. switch scope {
  150. case model.ApiScopeMonitor:
  151. if _, allowed := monitorScopeAllow[rel]; allowed && (c.Request.Method == http.MethodGet || c.Request.Method == http.MethodHead) {
  152. c.Next()
  153. return
  154. }
  155. case model.ApiScopeNodeSync:
  156. if scopeAllows(nodeSyncScopeAllow, rel, c.Request.Method) {
  157. c.Next()
  158. return
  159. }
  160. case model.ApiScopeNodeAdmin:
  161. if scopeAllows(nodeSyncScopeAllow, rel, c.Request.Method) || scopeAllows(nodeAdminScopeAllow, rel, c.Request.Method) {
  162. c.Next()
  163. return
  164. }
  165. default:
  166. deny()
  167. return
  168. }
  169. deny()
  170. }
  171. func relAPIPath(fullPath string) string {
  172. const marker = "/panel/api"
  173. _, after, ok := strings.Cut(fullPath, marker)
  174. if !ok {
  175. return ""
  176. }
  177. return after
  178. }
  179. // initRouter sets up the API routes for inbounds, server, and other endpoints.
  180. func (a *APIController) initRouter(g *gin.RouterGroup) {
  181. // Main API group
  182. api := g.Group("/panel/api")
  183. api.Use(a.checkAPIAuth)
  184. api.Use(a.enforceTokenScope)
  185. // Decode + verify the node config envelope (zstd + X-Config-Sha256) and
  186. // advertise support, before CSRF/handlers read the body.
  187. api.Use(middleware.ConfigEnvelopeMiddleware())
  188. api.Use(middleware.CSRFMiddleware())
  189. api.GET("/openapi.json", ServeOpenAPISpec)
  190. // Inbounds API
  191. inbounds := api.Group("/inbounds")
  192. a.inboundController = NewInboundController(inbounds)
  193. clients := api.Group("/clients")
  194. NewClientController(clients)
  195. NewGroupController(clients)
  196. // Server API
  197. server := api.Group("/server")
  198. a.serverController = NewServerController(server)
  199. // Nodes API — multi-panel management
  200. nodes := api.Group("/nodes")
  201. a.nodeController = NewNodeController(nodes)
  202. // Hosts API — per-inbound override endpoints for subscription links
  203. hosts := api.Group("/hosts")
  204. a.hostController = NewHostController(hosts)
  205. // Settings + Xray config management live under the API surface too, so the
  206. // same API token drives them. Paths are /panel/api/setting/* and
  207. // /panel/api/xray/*.
  208. a.settingController = NewSettingController(api)
  209. a.xraySettingController = NewXraySettingController(api)
  210. // Subscription balancers — client-side balancers for the JSON sub output
  211. NewSubBalancerController(api)
  212. // Extra routes
  213. api.POST("/backuptotgbot", a.BackuptoTgbot)
  214. }
  215. // BackuptoTgbot sends a backup of the panel data to Telegram bot admins.
  216. func (a *APIController) BackuptoTgbot(c *gin.Context) {
  217. a.Tgbot.SendBackupToAdmins()
  218. }