3x-ui/node/api/server.go

178 lines
4.3 KiB
Go
Raw Normal View History

2026-01-05 21:12:53 +00:00
// Package api provides REST API endpoints for the node service.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/node/xray"
"github.com/gin-gonic/gin"
)
// Server provides REST API for managing the node.
type Server struct {
port int
apiKey string
xrayManager *xray.Manager
httpServer *http.Server
}
// NewServer creates a new API server instance.
func NewServer(port int, apiKey string, xrayManager *xray.Manager) *Server {
return &Server{
port: port,
apiKey: apiKey,
xrayManager: xrayManager,
}
}
// Start starts the HTTP server.
func (s *Server) Start() error {
gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.Use(gin.Recovery())
router.Use(s.authMiddleware())
// Health check endpoint (no auth required)
router.GET("/health", s.health)
// API endpoints (require auth)
api := router.Group("/api/v1")
{
api.POST("/apply-config", s.applyConfig)
api.POST("/reload", s.reload)
2026-01-06 00:08:00 +00:00
api.POST("/force-reload", s.forceReload)
2026-01-05 21:12:53 +00:00
api.GET("/status", s.status)
2026-01-05 23:27:12 +00:00
api.GET("/stats", s.stats)
2026-01-05 21:12:53 +00:00
}
s.httpServer = &http.Server{
Addr: fmt.Sprintf(":%d", s.port),
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
logger.Infof("API server listening on port %d", s.port)
return s.httpServer.ListenAndServe()
}
// Stop stops the HTTP server.
func (s *Server) Stop() error {
if s.httpServer == nil {
return nil
}
return s.httpServer.Close()
}
// authMiddleware validates API key from Authorization header.
func (s *Server) authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Skip auth for health endpoint
if c.Request.URL.Path == "/health" {
c.Next()
return
}
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing Authorization header"})
c.Abort()
return
}
// Support both "Bearer <key>" and direct key
apiKey := authHeader
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
apiKey = authHeader[7:]
}
if apiKey != s.apiKey {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid API key"})
c.Abort()
return
}
c.Next()
}
}
// health returns the health status of the node.
func (s *Server) health(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"service": "3x-ui-node",
})
}
// applyConfig applies a new XRAY configuration.
func (s *Server) applyConfig(c *gin.Context) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"})
return
}
// Validate JSON
var configJSON json.RawMessage
if err := json.Unmarshal(body, &configJSON); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"})
return
}
if err := s.xrayManager.ApplyConfig(body); err != nil {
logger.Errorf("Failed to apply config: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Configuration applied successfully"})
}
// reload reloads XRAY configuration.
func (s *Server) reload(c *gin.Context) {
if err := s.xrayManager.Reload(); err != nil {
logger.Errorf("Failed to reload: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "XRAY reloaded successfully"})
}
2026-01-06 00:08:00 +00:00
// forceReload forcefully reloads XRAY even if it's hung or not running.
func (s *Server) forceReload(c *gin.Context) {
if err := s.xrayManager.ForceReload(); err != nil {
logger.Errorf("Failed to force reload: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "XRAY force reloaded successfully"})
}
2026-01-05 21:12:53 +00:00
// status returns the current status of XRAY.
func (s *Server) status(c *gin.Context) {
status := s.xrayManager.GetStatus()
c.JSON(http.StatusOK, status)
}
2026-01-05 23:27:12 +00:00
// stats returns traffic and online clients statistics from XRAY.
func (s *Server) stats(c *gin.Context) {
// Get reset parameter (default: false)
reset := c.DefaultQuery("reset", "false") == "true"
stats, err := s.xrayManager.GetStats(reset)
if err != nil {
logger.Errorf("Failed to get stats: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}