mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-01-13 01:02:46 +00:00
177 lines
4.3 KiB
Go
177 lines
4.3 KiB
Go
// 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)
|
|
api.POST("/force-reload", s.forceReload)
|
|
api.GET("/status", s.status)
|
|
api.GET("/stats", s.stats)
|
|
}
|
|
|
|
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"})
|
|
}
|
|
|
|
// 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"})
|
|
}
|
|
|
|
// status returns the current status of XRAY.
|
|
func (s *Server) status(c *gin.Context) {
|
|
status := s.xrayManager.GetStatus()
|
|
c.JSON(http.StatusOK, status)
|
|
}
|
|
|
|
// 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)
|
|
}
|