mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-01-13 09:12:44 +00:00
317 lines
8.6 KiB
Go
317 lines
8.6 KiB
Go
|
|
// Package service provides Node management service for multi-node architecture.
|
||
|
|
package service
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||
|
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||
|
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||
|
|
)
|
||
|
|
|
||
|
|
// NodeService provides business logic for managing nodes in multi-node mode.
|
||
|
|
type NodeService struct{}
|
||
|
|
|
||
|
|
// GetAllNodes retrieves all nodes from the database.
|
||
|
|
func (s *NodeService) GetAllNodes() ([]*model.Node, error) {
|
||
|
|
db := database.GetDB()
|
||
|
|
var nodes []*model.Node
|
||
|
|
err := db.Find(&nodes).Error
|
||
|
|
return nodes, err
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetNode retrieves a node by ID.
|
||
|
|
func (s *NodeService) GetNode(id int) (*model.Node, error) {
|
||
|
|
db := database.GetDB()
|
||
|
|
var node model.Node
|
||
|
|
err := db.First(&node, id).Error
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return &node, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// AddNode creates a new node.
|
||
|
|
func (s *NodeService) AddNode(node *model.Node) error {
|
||
|
|
db := database.GetDB()
|
||
|
|
return db.Create(node).Error
|
||
|
|
}
|
||
|
|
|
||
|
|
// UpdateNode updates an existing node.
|
||
|
|
func (s *NodeService) UpdateNode(node *model.Node) error {
|
||
|
|
db := database.GetDB()
|
||
|
|
return db.Save(node).Error
|
||
|
|
}
|
||
|
|
|
||
|
|
// DeleteNode deletes a node by ID.
|
||
|
|
// This will cascade delete all InboundNodeMapping entries for this node.
|
||
|
|
func (s *NodeService) DeleteNode(id int) error {
|
||
|
|
db := database.GetDB()
|
||
|
|
|
||
|
|
// Delete all node mappings for this node (cascade delete)
|
||
|
|
err := db.Where("node_id = ?", id).Delete(&model.InboundNodeMapping{}).Error
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// Delete the node itself
|
||
|
|
return db.Delete(&model.Node{}, id).Error
|
||
|
|
}
|
||
|
|
|
||
|
|
// CheckNodeHealth checks if a node is online and updates its status.
|
||
|
|
func (s *NodeService) CheckNodeHealth(node *model.Node) error {
|
||
|
|
status, err := s.CheckNodeStatus(node)
|
||
|
|
if err != nil {
|
||
|
|
node.Status = "error"
|
||
|
|
node.LastCheck = time.Now().Unix()
|
||
|
|
s.UpdateNode(node)
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
node.Status = status
|
||
|
|
node.LastCheck = time.Now().Unix()
|
||
|
|
return s.UpdateNode(node)
|
||
|
|
}
|
||
|
|
|
||
|
|
// CheckNodeStatus performs a health check on a given node.
|
||
|
|
func (s *NodeService) CheckNodeStatus(node *model.Node) (string, error) {
|
||
|
|
client := &http.Client{
|
||
|
|
Timeout: 5 * time.Second,
|
||
|
|
}
|
||
|
|
|
||
|
|
url := fmt.Sprintf("%s/health", node.Address)
|
||
|
|
resp, err := client.Get(url)
|
||
|
|
if err != nil {
|
||
|
|
return "offline", err
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
if resp.StatusCode == http.StatusOK {
|
||
|
|
return "online", nil
|
||
|
|
}
|
||
|
|
return "error", fmt.Errorf("node returned status code %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// CheckAllNodesHealth checks health of all nodes.
|
||
|
|
func (s *NodeService) CheckAllNodesHealth() {
|
||
|
|
nodes, err := s.GetAllNodes()
|
||
|
|
if err != nil {
|
||
|
|
logger.Errorf("Failed to get nodes for health check: %v", err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, node := range nodes {
|
||
|
|
go s.CheckNodeHealth(node)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetNodeForInbound returns the node assigned to an inbound, or nil if not assigned.
|
||
|
|
// Deprecated: Use GetNodesForInbound for multi-node support.
|
||
|
|
func (s *NodeService) GetNodeForInbound(inboundId int) (*model.Node, error) {
|
||
|
|
db := database.GetDB()
|
||
|
|
var mapping model.InboundNodeMapping
|
||
|
|
err := db.Where("inbound_id = ?", inboundId).First(&mapping).Error
|
||
|
|
if err != nil {
|
||
|
|
return nil, err // Not found is OK, means inbound is not assigned to any node
|
||
|
|
}
|
||
|
|
|
||
|
|
return s.GetNode(mapping.NodeId)
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetNodesForInbound returns all nodes assigned to an inbound.
|
||
|
|
func (s *NodeService) GetNodesForInbound(inboundId int) ([]*model.Node, error) {
|
||
|
|
db := database.GetDB()
|
||
|
|
var mappings []model.InboundNodeMapping
|
||
|
|
err := db.Where("inbound_id = ?", inboundId).Find(&mappings).Error
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
nodes := make([]*model.Node, 0, len(mappings))
|
||
|
|
for _, mapping := range mappings {
|
||
|
|
node, err := s.GetNode(mapping.NodeId)
|
||
|
|
if err == nil && node != nil {
|
||
|
|
nodes = append(nodes, node)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nodes, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetInboundsForNode returns all inbounds assigned to a node.
|
||
|
|
func (s *NodeService) GetInboundsForNode(nodeId int) ([]*model.Inbound, error) {
|
||
|
|
db := database.GetDB()
|
||
|
|
var mappings []model.InboundNodeMapping
|
||
|
|
err := db.Where("node_id = ?", nodeId).Find(&mappings).Error
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
inbounds := make([]*model.Inbound, 0, len(mappings))
|
||
|
|
for _, mapping := range mappings {
|
||
|
|
var inbound model.Inbound
|
||
|
|
err := db.First(&inbound, mapping.InboundId).Error
|
||
|
|
if err == nil {
|
||
|
|
inbounds = append(inbounds, &inbound)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return inbounds, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// AssignInboundToNode assigns an inbound to a node.
|
||
|
|
func (s *NodeService) AssignInboundToNode(inboundId, nodeId int) error {
|
||
|
|
db := database.GetDB()
|
||
|
|
mapping := &model.InboundNodeMapping{
|
||
|
|
InboundId: inboundId,
|
||
|
|
NodeId: nodeId,
|
||
|
|
}
|
||
|
|
return db.Save(mapping).Error
|
||
|
|
}
|
||
|
|
|
||
|
|
// AssignInboundToNodes assigns an inbound to multiple nodes.
|
||
|
|
func (s *NodeService) AssignInboundToNodes(inboundId int, nodeIds []int) error {
|
||
|
|
db := database.GetDB()
|
||
|
|
// First, remove all existing assignments
|
||
|
|
if err := db.Where("inbound_id = ?", inboundId).Delete(&model.InboundNodeMapping{}).Error; err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// Then, create new assignments
|
||
|
|
for _, nodeId := range nodeIds {
|
||
|
|
if nodeId > 0 {
|
||
|
|
mapping := &model.InboundNodeMapping{
|
||
|
|
InboundId: inboundId,
|
||
|
|
NodeId: nodeId,
|
||
|
|
}
|
||
|
|
if err := db.Create(mapping).Error; err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// UnassignInboundFromNode removes the assignment of an inbound from its node.
|
||
|
|
func (s *NodeService) UnassignInboundFromNode(inboundId int) error {
|
||
|
|
db := database.GetDB()
|
||
|
|
return db.Where("inbound_id = ?", inboundId).Delete(&model.InboundNodeMapping{}).Error
|
||
|
|
}
|
||
|
|
|
||
|
|
// ApplyConfigToNode sends XRAY configuration to a node.
|
||
|
|
func (s *NodeService) ApplyConfigToNode(node *model.Node, xrayConfig []byte) error {
|
||
|
|
client := &http.Client{
|
||
|
|
Timeout: 30 * time.Second,
|
||
|
|
}
|
||
|
|
|
||
|
|
url := fmt.Sprintf("%s/api/v1/apply-config", node.Address)
|
||
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(xrayConfig))
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
req.Header.Set("Content-Type", "application/json")
|
||
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", node.ApiKey))
|
||
|
|
|
||
|
|
resp, err := client.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
if resp.StatusCode != http.StatusOK {
|
||
|
|
body, _ := io.ReadAll(resp.Body)
|
||
|
|
return fmt.Errorf("node returned status %d: %s", resp.StatusCode, string(body))
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// ValidateApiKey validates the API key by making a test request to the node.
|
||
|
|
func (s *NodeService) ValidateApiKey(node *model.Node) error {
|
||
|
|
client := &http.Client{
|
||
|
|
Timeout: 5 * time.Second,
|
||
|
|
}
|
||
|
|
|
||
|
|
// First, check if node is reachable via health endpoint
|
||
|
|
healthURL := fmt.Sprintf("%s/health", node.Address)
|
||
|
|
healthResp, err := client.Get(healthURL)
|
||
|
|
if err != nil {
|
||
|
|
logger.Errorf("Failed to connect to node %s at %s: %v", node.Address, healthURL, err)
|
||
|
|
return fmt.Errorf("failed to connect to node: %v", err)
|
||
|
|
}
|
||
|
|
healthResp.Body.Close()
|
||
|
|
|
||
|
|
if healthResp.StatusCode != http.StatusOK {
|
||
|
|
return fmt.Errorf("node health check failed with status %d", healthResp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try to get node status - this will validate the API key
|
||
|
|
url := fmt.Sprintf("%s/api/v1/status", node.Address)
|
||
|
|
req, err := http.NewRequest("GET", url, nil)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("failed to create request: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
authHeader := fmt.Sprintf("Bearer %s", node.ApiKey)
|
||
|
|
req.Header.Set("Authorization", authHeader)
|
||
|
|
|
||
|
|
logger.Debugf("Validating API key for node %s at %s (key: %s)", node.Name, url, node.ApiKey)
|
||
|
|
|
||
|
|
resp, err := client.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
logger.Errorf("Failed to connect to node %s: %v", node.Address, err)
|
||
|
|
return fmt.Errorf("failed to connect to node: %v", err)
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
body, _ := io.ReadAll(resp.Body)
|
||
|
|
|
||
|
|
if resp.StatusCode == http.StatusUnauthorized {
|
||
|
|
logger.Warningf("Invalid API key for node %s (sent: %s): %s", node.Address, authHeader, string(body))
|
||
|
|
return fmt.Errorf("invalid API key")
|
||
|
|
}
|
||
|
|
|
||
|
|
if resp.StatusCode != http.StatusOK {
|
||
|
|
logger.Errorf("Node %s returned status %d: %s", node.Address, resp.StatusCode, string(body))
|
||
|
|
return fmt.Errorf("node returned status %d: %s", resp.StatusCode, string(body))
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.Debugf("API key validated successfully for node %s", node.Name)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetNodeStatus retrieves the status of a node.
|
||
|
|
func (s *NodeService) GetNodeStatus(node *model.Node) (map[string]interface{}, error) {
|
||
|
|
client := &http.Client{
|
||
|
|
Timeout: 5 * time.Second,
|
||
|
|
}
|
||
|
|
|
||
|
|
url := fmt.Sprintf("%s/api/v1/status", node.Address)
|
||
|
|
req, err := http.NewRequest("GET", url, nil)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", node.ApiKey))
|
||
|
|
|
||
|
|
resp, err := client.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
if resp.StatusCode != http.StatusOK {
|
||
|
|
return nil, fmt.Errorf("node returned status %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
var status map[string]interface{}
|
||
|
|
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
return status, nil
|
||
|
|
}
|