Files
3x-ui-agent/internal/web/controller/api.go
T
Rogue 29de9761c8
CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
Docs CI / build (push) Waiting to run
Docs Deploy (GitHub Pages) / build (push) Waiting to run
Docs Deploy (GitHub Pages) / deploy (push) Blocked by required conditions
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
V2.0.1: listen override fix, sing-box core fetch, probe settings page + fast report, spx removal, core mem env
- 0: sing-box gen overrides fragment listen/port from the inbound row
  (empty listen -> "::"), Update/Add recompute the stored fragment;
  non-loopback E2E against the real core binary.
- 1: agent fetches the sing-box core from the Gitea release when
  missing, self-checks it, and only then latches the switch.
- 2: node list shows the sing-box core version (heartbeat field,
  persisted on the node row), xray version as fallback.
- 3: public-page fallback returns 404 to anonymous visitors when the
  page is disabled.
- 4: settings restart hint bar removed, dead i18n keys cleaned.
- 5: two PUT calls missing JSON headers fixed (audit: only those two),
  batch probe-task creation endpoint + UI.
- 6: cf-probe retirement card and coverage pipeline removed.
- 7: generated vless links no longer carry spx (panel export + sub);
  parser still accepts spx; golden fixtures updated.
- probe batch: dedicated probe settings tab (public-page settings,
  tasks, per-node meta), retention_days editable, per-node traffic
  calibration + stat-interface pinning, runtime metrics split into a
  lightweight fast report (default 3s, 1-60s adjustable).
- 8: sing-box core processes start with GOMEMLIMIT=32MiB, GOGC=50.
- 9: dead /getTwoFactorEnable docs entries removed (frontend call and
  contract entry already gone at base; verified by grep).
2026-10-23 07:53:11 +00:00

229 lines
7.7 KiB
Go

package controller
import (
"net/http"
"strings"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
"github.com/mhsanaei/3x-ui/v3/internal/web/service/panel"
"github.com/mhsanaei/3x-ui/v3/internal/web/service/tgbot"
"github.com/mhsanaei/3x-ui/v3/internal/web/session"
"github.com/gin-gonic/gin"
)
// APIController handles the main API routes for the 3x-ui panel, including inbounds and server management.
type APIController struct {
BaseController
inboundController *InboundController
serverController *ServerController
nodeController *NodeController
agentController *AgentController
hostController *HostController
settingController *SettingController
userService panel.UserService
apiTokenService panel.ApiTokenService
Tgbot tgbot.Tgbot
}
// NewAPIController creates a new APIController instance and initializes its routes.
func NewAPIController(g *gin.RouterGroup) *APIController {
a := &APIController{}
a.initRouter(g)
return a
}
func (a *APIController) checkAPIAuth(c *gin.Context) {
// A verified client certificate (a completed mTLS handshake) authenticates
// the caller, equivalent to a valid bearer token. api_authed must be set so
// the CSRF middleware lets cert-authed mutations through.
if c.Request.TLS != nil && len(c.Request.TLS.VerifiedChains) > 0 {
if u, err := a.userService.GetFirstUser(); err == nil {
session.SetAPIAuthUser(c, u)
}
c.Set("api_authed", true)
c.Set("api_token_scope", model.ApiScopeNodeSync)
c.Next()
return
}
auth := c.GetHeader("Authorization")
if after, ok := strings.CutPrefix(auth, "Bearer "); ok {
tok := after
if row, ok := a.apiTokenService.MatchToken(tok); ok {
if u, err := a.userService.GetFirstUser(); err == nil {
session.SetAPIAuthUser(c, u)
}
c.Set("api_authed", true)
c.Set("api_token_scope", row.Scope)
c.Next()
return
}
}
if !session.IsLogin(c) {
if c.GetHeader("X-Requested-With") == "XMLHttpRequest" {
c.AbortWithStatus(http.StatusUnauthorized)
} else {
c.AbortWithStatus(http.StatusNotFound)
}
return
}
c.Next()
}
// monitorScopeAllow exposes only status/metrics routes without sensitive data.
// Keys are route patterns relative to /panel/api.
var monitorScopeAllow = map[string]struct{}{
"/server/status": {},
"/server/getPanelUpdateInfo": {},
"/nodes/history/:id/:metric/:bucket": {},
}
// nodeSyncScopeAllow is the node-sync route/method allowlist relative to
// /panel/api; Gin patterns prevent concrete parameters broadening authority.
var nodeSyncScopeAllow = map[string]map[string]struct{}{
"/server/status": {http.MethodGet: {}},
"/inbounds/list": {http.MethodGet: {}},
"/inbounds/add": {http.MethodPost: {}},
"/inbounds/del/:id": {http.MethodPost: {}},
"/inbounds/update/:id": {http.MethodPost: {}},
"/clients/add": {http.MethodPost: {}},
"/clients/del/:email": {http.MethodPost: {}},
"/clients/:email/detach": {http.MethodPost: {}},
"/clients/update/:email": {http.MethodPost: {}},
"/server/getWebCertFiles": {http.MethodGet: {}},
"/server/descendants": {http.MethodGet: {}},
"/clients/resetTraffic/:email": {http.MethodPost: {}},
"/inbounds/resetAllTraffics": {http.MethodPost: {}},
"/inbounds/:id/resetTraffic": {http.MethodPost: {}},
"/clients/onlinesByGuid": {http.MethodPost: {}},
"/clients/onlines": {http.MethodPost: {}},
"/clients/lastOnline": {http.MethodPost: {}},
"/inbounds/pushClientTraffics": {http.MethodPost: {}},
"/server/clientIps": {http.MethodGet: {}, http.MethodPost: {}},
"/clients/clientIpsByGuid": {http.MethodPost: {}},
"/hosts/list": {http.MethodGet: {}},
}
// enforceTokenScope applies explicit allowlists to monitor and node-sync tokens.
// Admin tokens and session-login users retain their existing behavior.
func (a *APIController) enforceTokenScope(c *gin.Context) {
scopeVal, ok := c.Get("api_token_scope")
if !ok {
c.Next()
return
}
scope, _ := scopeVal.(string)
if scope == model.ApiScopeAdmin {
c.Next()
return
}
deny := func() {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"success": false,
"msg": "this API token is not permitted to access this endpoint",
})
}
rel := relAPIPath(c.FullPath())
switch scope {
case model.ApiScopeMonitor:
if _, allowed := monitorScopeAllow[rel]; allowed && (c.Request.Method == http.MethodGet || c.Request.Method == http.MethodHead) {
c.Next()
return
}
case model.ApiScopeNodeSync:
if methods, allowed := nodeSyncScopeAllow[rel]; allowed {
if _, allowedMethod := methods[c.Request.Method]; allowedMethod {
c.Next()
return
}
}
default:
deny()
return
}
deny()
}
func relAPIPath(fullPath string) string {
const marker = "/panel/api"
_, after, ok := strings.Cut(fullPath, marker)
if !ok {
return ""
}
return after
}
// initRouter sets up the API routes for inbounds, server, and other endpoints.
func (a *APIController) initRouter(g *gin.RouterGroup) {
// Main API group
api := g.Group("/panel/api")
api.Use(a.checkAPIAuth)
api.Use(a.enforceTokenScope)
// Decode + verify the node config envelope (zstd + X-Config-Sha256) and
// advertise support, before CSRF/handlers read the body.
api.Use(middleware.ConfigEnvelopeMiddleware())
api.Use(middleware.CSRFMiddleware())
// Inbounds API
inbounds := api.Group("/inbounds")
a.inboundController = NewInboundController(inbounds)
clients := api.Group("/clients")
NewClientController(clients)
// Server API
server := api.Group("/server")
a.serverController = NewServerController(server)
// Nodes API — multi-panel management
nodes := api.Group("/nodes")
a.nodeController = NewNodeController(nodes)
// Agent API — lightweight agent mode (no web panel on node).
// The agent endpoints (register/heartbeat/config/traffic) authenticate with
// the agent's own Bearer token, which is NOT a panel API token, so they are
// mounted on a public group and must not pass through checkAPIAuth (it would
// 404 every agent call: no session, no panel token).
agentPub := g.Group("/panel/api/agent")
a.agentController = NewAgentController(agentPub)
// Agent administration — panel-authenticated (session login or panel API
// token via the group's checkAPIAuth): token issuance and per-agent egress
// management. Agent Bearer tokens cannot pass checkAPIAuth, so agents can
// never reach these.
agentAdmin := api.Group("/agent")
a.agentController.RegisterAdminRoutes(agentAdmin)
// Item 7 B3: probe metadata editing rides the same admin group
// (/panel/api/agent/probe-meta/:nodeId).
NewProbeMetaController(agentAdmin)
// Item 7 B4: probe task (latency target) management, same group
// (/panel/api/agent/probe-tasks*).
NewProbeTasksController(agentAdmin)
// Item 7 B5: theme store + public-surface settings, on the API
// group itself (/panel/api/probe/*).
NewProbeThemesController(api)
// Item 7 B6: notification settings + test send, same group
// (/panel/api/probe/notify*).
NewProbeNotifyController(api)
// Hosts API — per-inbound override endpoints for subscription links
hosts := api.Group("/hosts")
a.hostController = NewHostController(hosts)
// Settings management lives under the API surface too, so the same
// API token drives it. Paths are /panel/api/setting/*. (The xray
// config surface retired with the xray settings page, ITEM6 B4.)
a.settingController = NewSettingController(api)
// Extra routes
api.POST("/backuptotgbot", a.BackuptoTgbot)
}
// BackuptoTgbot sends a backup of the panel data to Telegram bot admins.
func (a *APIController) BackuptoTgbot(c *gin.Context) {
a.Tgbot.SendBackupToAdmins()
}