Files
mmw-agent/internal/handler/management_handler.go
T
2026-04-24 09:49:57 +08:00

2822 lines
75 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"mmw-agent/internal/constants"
"mmw-agent/internal/xrpc"
"github.com/xtls/xray-core/app/proxyman/command"
"github.com/xtls/xray-core/infra/conf"
)
var nginxInstalling atomic.Bool
// ManageHandler 处理子端管理接口请求。
type ManageHandler struct {
configToken string
}
// 创建管理处理器。
func NewManageHandler(configToken string) *ManageHandler {
return &ManageHandler{
configToken: configToken,
}
}
// 校验请求身份(token + User-Agent)。
func (h *ManageHandler) authenticate(r *http.Request) bool {
if r.Header.Get(constants.HeaderUserAgent) != constants.AgentUserAgent {
return false
}
if h.configToken == "" {
return true
}
auth := r.Header.Get(constants.HeaderAuthorization)
if auth == "" {
auth = r.Header.Get(constants.HeaderMMRemoteToken)
}
if auth == "" {
return false
}
if strings.HasPrefix(auth, constants.BearerPrefix) {
token := strings.TrimPrefix(auth, constants.BearerPrefix)
return token == h.configToken
}
return auth == h.configToken
}
// 输出 JSON 响应。
func writeJSON(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
// 输出错误响应。
func writeError(w http.ResponseWriter, statusCode int, message string) {
writeJSON(w, statusCode, map[string]interface{}{
"success": false,
"error": message,
})
}
// ================== 系统服务状态 ==================
// ServicesStatusResponse 表示服务状态查询响应。
type ServicesStatusResponse struct {
Success bool `json:"success"`
Xray *ServiceStatus `json:"xray,omitempty"`
Nginx *ServiceStatus `json:"nginx,omitempty"`
}
// ServiceStatus 表示单个服务状态。
type ServiceStatus struct {
Installed bool `json:"installed"`
Running bool `json:"running"`
Version string `json:"version,omitempty"`
}
// 处理 GET /api/child/services/status。
func (h *ManageHandler) HandleServicesStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
response := ServicesStatusResponse{
Success: true,
Xray: h.getXrayStatus(),
Nginx: h.getNginxStatus(),
}
writeJSON(w, http.StatusOK, response)
}
func (h *ManageHandler) getXrayStatus() *ServiceStatus {
status := &ServiceStatus{}
xrayPath, err := exec.LookPath("xray")
if err != nil {
for _, p := range constants.XrayBinarySearchPaths {
if _, err := os.Stat(p); err == nil {
xrayPath = p
break
}
}
}
if xrayPath != "" {
status.Installed = true
cmd := exec.Command(xrayPath, "version")
output, err := cmd.Output()
if err == nil {
lines := strings.Split(string(output), "\n")
if len(lines) > 0 {
status.Version = strings.TrimSpace(lines[0])
}
}
}
// 优先使用 systemctl 检查
cmd := exec.Command("systemctl", "is-active", "xray")
output, _ := cmd.Output()
if strings.TrimSpace(string(output)) == "active" {
status.Running = true
return status
}
// 兜底:用 pgrep 检查 xray 进程
pgrepCmd := exec.Command("pgrep", "-x", "xray")
if err := pgrepCmd.Run(); err == nil {
status.Running = true
return status
}
// 兜底:用 ps 检查包含 "xray" 的进程
psCmd := exec.Command("bash", "-c", "ps aux | grep -v grep | grep -E '[x]ray' | head -1")
if output, err := psCmd.Output(); err == nil && len(strings.TrimSpace(string(output))) > 0 {
status.Running = true
}
return status
}
func (h *ManageHandler) getNginxStatus() *ServiceStatus {
status := &ServiceStatus{}
// 先查 PATH,再查编译安装路径
nginxPath, err := exec.LookPath("nginx")
if err != nil {
for _, candidate := range constants.NginxBinarySearchPaths {
if strings.Contains(candidate, "/") {
if _, statErr := os.Stat(candidate); statErr == nil {
nginxPath = candidate
err = nil
break
}
}
}
}
if err == nil {
status.Installed = true
cmd := exec.Command(nginxPath, "-v")
output, err := cmd.CombinedOutput()
if err == nil {
status.Version = strings.TrimSpace(string(output))
}
}
if nginxInstalling.Load() {
status.Version = "安装中..."
}
// 优先使用 systemctl 检查
cmd := exec.Command("systemctl", "is-active", "nginx")
output, _ := cmd.Output()
if strings.TrimSpace(string(output)) == "active" {
status.Running = true
return status
}
// 兜底:用 pgrep 检查 nginx 进程
pgrepCmd := exec.Command("pgrep", "-x", "nginx")
if err := pgrepCmd.Run(); err == nil {
status.Running = true
return status
}
// 兜底:用 ps 检查 nginx master 进程
psCmd := exec.Command("bash", "-c", "ps aux | grep -v grep | grep -E 'nginx: master' | head -1")
if output, err := psCmd.Output(); err == nil && len(strings.TrimSpace(string(output))) > 0 {
status.Running = true
}
return status
}
// ================== 服务控制 ==================
// ServiceControlRequest 表示服务控制请求。
type ServiceControlRequest struct {
Service string `json:"service"`
Action string `json:"action"`
}
// 处理 POST /api/child/services/control。
func (h *ManageHandler) HandleServiceControl(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
var req ServiceControlRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid request body")
return
}
if req.Service != "xray" && req.Service != "nginx" {
writeError(w, http.StatusBadRequest, "Invalid service. Must be 'xray' or 'nginx'")
return
}
if req.Action != "start" && req.Action != "stop" && req.Action != "restart" {
writeError(w, http.StatusBadRequest, "Invalid action. Must be 'start', 'stop', or 'restart'")
return
}
cmd := exec.Command("systemctl", req.Action, req.Service)
output, err := cmd.CombinedOutput()
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to %s %s: %v - %s", req.Action, req.Service, err, string(output)))
return
}
log.Printf("[Manage] Service %s: %s", req.Service, req.Action)
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": fmt.Sprintf("Service %s %sed successfully", req.Service, req.Action),
})
}
// ================== Xray 安装 ==================
// 处理 POST /api/child/xray/install。
func (h *ManageHandler) HandleXrayInstall(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
log.Printf("[Manage] Installing Xray...")
cmd := exec.Command("bash", "-c", "bash -c \"$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)\" @ install")
cmd.Env = os.Environ()
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
log.Printf("[Manage] Xray installation failed: %v, stderr: %s", err, stderr.String())
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Installation failed: %v", err))
return
}
log.Printf("[Manage] Xray installed successfully")
// 若无配置则下发默认配置
h.deployDefaultXrayConfig()
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Xray installed successfully",
"output": stdout.String(),
})
}
// 处理 POST /api/child/xray/remove。
func (h *ManageHandler) HandleXrayRemove(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
log.Printf("[Manage] Removing Xray...")
cmd := exec.Command("bash", "-c", "bash -c \"$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)\" @ remove")
cmd.Env = os.Environ()
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
log.Printf("[Manage] Xray removal failed: %v, stderr: %s", err, stderr.String())
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Removal failed: %v", err))
return
}
log.Printf("[Manage] Xray removed successfully")
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Xray removed successfully",
"output": stdout.String(),
})
}
// ================== Xray 配置 ==================
// 处理 GET/POST /api/child/xray/config。
func (h *ManageHandler) HandleXrayConfig(w http.ResponseWriter, r *http.Request) {
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
switch r.Method {
case http.MethodGet:
h.getXrayConfig(w, r)
case http.MethodPost:
h.setXrayConfig(w, r)
default:
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
}
}
func (h *ManageHandler) getXrayConfig(w http.ResponseWriter, r *http.Request) {
configPaths := constants.DefaultXrayConfigPaths
var configPath string
var content []byte
var err error
for _, p := range configPaths {
content, err = os.ReadFile(p)
if err == nil {
configPath = p
break
}
}
if configPath == "" {
writeError(w, http.StatusNotFound, "Xray config not found")
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"path": configPath,
"config": string(content),
})
}
func (h *ManageHandler) setXrayConfig(w http.ResponseWriter, r *http.Request) {
var req struct {
Config string `json:"config"`
Path string `json:"path,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid request body")
return
}
var js json.RawMessage
if err := json.Unmarshal([]byte(req.Config), &js); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON config")
return
}
configPath := req.Path
if configPath == "" {
configPath = constants.DefaultXrayConfigPaths[0]
}
dir := filepath.Dir(configPath)
if err := os.MkdirAll(dir, 0755); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create directory: %v", err))
return
}
if err := os.WriteFile(configPath, []byte(req.Config), 0644); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to write config: %v", err))
return
}
log.Printf("[Manage] Xray config saved to %s", configPath)
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Config saved successfully",
"path": configPath,
})
}
// ================== Xray 系统配置 ==================
// XraySystemConfig 表示 Xray 系统配置状态。
type XraySystemConfig struct {
MetricsEnabled bool `json:"metrics_enabled"`
MetricsListen string `json:"metrics_listen"`
StatsEnabled bool `json:"stats_enabled"`
GrpcEnabled bool `json:"grpc_enabled"`
GrpcPort int `json:"grpc_port"`
}
// 处理 GET/POST /api/child/xray/system-config。
func (h *ManageHandler) HandleXraySystemConfig(w http.ResponseWriter, r *http.Request) {
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
switch r.Method {
case http.MethodGet:
h.getXraySystemConfig(w, r)
case http.MethodPost:
h.updateXraySystemConfig(w, r)
default:
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
}
}
func (h *ManageHandler) getXraySystemConfig(w http.ResponseWriter, r *http.Request) {
configPath := h.findXrayConfigPath()
if configPath == "" {
writeError(w, http.StatusNotFound, "Xray config not found")
return
}
content, err := os.ReadFile(configPath)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read config: %v", err))
return
}
var config map[string]interface{}
if err := json.Unmarshal(content, &config); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Invalid JSON: %v", err))
return
}
sysConfig := &XraySystemConfig{
MetricsListen: constants.DefaultMetricsListen,
GrpcPort: 46736,
}
if metrics, ok := config["metrics"].(map[string]interface{}); ok {
sysConfig.MetricsEnabled = true
if listen, ok := metrics["listen"].(string); ok {
sysConfig.MetricsListen = listen
}
}
if _, ok := config["stats"]; ok {
sysConfig.StatsEnabled = true
}
if api, ok := config["api"].(map[string]interface{}); ok {
if _, hasTag := api["tag"]; hasTag {
sysConfig.GrpcEnabled = true
}
}
if inbounds, ok := config["inbounds"].([]interface{}); ok {
for _, ib := range inbounds {
if inbound, ok := ib.(map[string]interface{}); ok {
if tag, _ := inbound["tag"].(string); tag == "api" {
if port, ok := inbound["port"].(float64); ok {
sysConfig.GrpcPort = int(port)
}
break
}
}
}
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"config": sysConfig,
})
}
func (h *ManageHandler) updateXraySystemConfig(w http.ResponseWriter, r *http.Request) {
var req XraySystemConfig
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid request body")
return
}
configPath := h.findXrayConfigPath()
if configPath == "" {
writeError(w, http.StatusNotFound, "Xray config not found")
return
}
content, err := os.ReadFile(configPath)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read config: %v", err))
return
}
var config map[string]interface{}
if err := json.Unmarshal(content, &config); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Invalid JSON: %v", err))
return
}
if req.MetricsEnabled {
config["metrics"] = map[string]interface{}{
"tag": "Metrics",
"listen": req.MetricsListen,
}
} else {
delete(config, "metrics")
}
if req.StatsEnabled {
config["stats"] = map[string]interface{}{}
config["policy"] = map[string]interface{}{
"levels": map[string]interface{}{
"0": map[string]interface{}{
"handshake": float64(5),
"connIdle": float64(300),
"uplinkOnly": float64(2),
"downlinkOnly": float64(2),
"statsUserUplink": true,
"statsUserDownlink": true,
},
},
"system": map[string]interface{}{
"statsInboundUplink": true,
"statsInboundDownlink": true,
"statsOutboundUplink": true,
"statsOutboundDownlink": true,
},
}
} else {
delete(config, "stats")
delete(config, "policy")
}
if req.GrpcEnabled {
config["api"] = map[string]interface{}{
"tag": "api",
"services": []interface{}{"HandlerService", "LoggerService", "StatsService", "RoutingService"},
}
hasAPIInbound := false
if inbounds, ok := config["inbounds"].([]interface{}); ok {
for i, ib := range inbounds {
if inbound, ok := ib.(map[string]interface{}); ok {
if tag, _ := inbound["tag"].(string); tag == "api" {
inbound["port"] = float64(req.GrpcPort)
inbounds[i] = inbound
hasAPIInbound = true
break
}
}
}
if !hasAPIInbound {
apiInbound := map[string]interface{}{
"tag": "api",
"port": float64(req.GrpcPort),
"listen": constants.LocalhostIP,
"protocol": "dokodemo-door",
"settings": map[string]interface{}{
"address": constants.LocalhostIP,
},
}
config["inbounds"] = append([]interface{}{apiInbound}, inbounds...)
}
} else {
config["inbounds"] = []interface{}{
map[string]interface{}{
"tag": "api",
"port": float64(req.GrpcPort),
"listen": constants.LocalhostIP,
"protocol": "dokodemo-door",
"settings": map[string]interface{}{
"address": constants.LocalhostIP,
},
},
}
}
h.ensureAPIRoutingRule(config)
} else {
delete(config, "api")
if inbounds, ok := config["inbounds"].([]interface{}); ok {
newInbounds := make([]interface{}, 0)
for _, ib := range inbounds {
if inbound, ok := ib.(map[string]interface{}); ok {
if tag, _ := inbound["tag"].(string); tag != "api" {
newInbounds = append(newInbounds, inbound)
}
}
}
config["inbounds"] = newInbounds
}
h.removeAPIRoutingRule(config)
}
backupPath := configPath + ".backup"
if err := os.WriteFile(backupPath, content, 0644); err != nil {
log.Printf("[Manage] Warning: failed to backup config: %v", err)
}
newContent, _ := json.MarshalIndent(config, "", " ")
if err := os.WriteFile(configPath, newContent, 0644); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to write config: %v", err))
return
}
cmd := exec.Command("systemctl", "restart", "xray")
if err := cmd.Run(); err != nil {
log.Printf("[Manage] Warning: failed to restart xray: %v", err)
}
log.Printf("[Manage] Xray system config updated: metrics=%v, stats=%v, grpc=%v",
req.MetricsEnabled, req.StatsEnabled, req.GrpcEnabled)
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "System config updated, Xray restarted",
})
}
func (h *ManageHandler) ensureAPIRoutingRule(config map[string]interface{}) {
routing, ok := config["routing"].(map[string]interface{})
if !ok {
routing = map[string]interface{}{
"domainStrategy": "IPIfNonMatch",
"rules": []interface{}{},
}
config["routing"] = routing
}
rules, ok := routing["rules"].([]interface{})
if !ok {
rules = []interface{}{}
}
for _, r := range rules {
if rule, ok := r.(map[string]interface{}); ok {
if outboundTag, _ := rule["outboundTag"].(string); outboundTag == "api" {
return
}
}
}
apiRule := map[string]interface{}{
"type": "field",
"inboundTag": []interface{}{"api"},
"outboundTag": "api",
}
routing["rules"] = append([]interface{}{apiRule}, rules...)
}
func (h *ManageHandler) removeAPIRoutingRule(config map[string]interface{}) {
routing, ok := config["routing"].(map[string]interface{})
if !ok {
return
}
rules, ok := routing["rules"].([]interface{})
if !ok {
return
}
newRules := make([]interface{}, 0)
for _, r := range rules {
if rule, ok := r.(map[string]interface{}); ok {
if outboundTag, _ := rule["outboundTag"].(string); outboundTag != "api" {
newRules = append(newRules, rule)
}
}
}
routing["rules"] = newRules
}
// ================== Nginx 安装 ==================
// 处理 POST /api/child/nginx/install。
func (h *ManageHandler) HandleNginxInstall(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
if nginxInstalling.Load() {
writeError(w, http.StatusConflict, "Nginx installation already in progress")
return
}
var req struct {
Domain string `json:"domain"`
}
json.NewDecoder(r.Body).Decode(&req)
log.Printf("[Manage] Starting Nginx installation (async)...")
nginxInstalling.Store(true)
go func() {
defer nginxInstalling.Store(false)
cmd := exec.Command("bash", "-c",
`curl -fsSL https://raw.githubusercontent.com/iluobei/miaomiaowuX/main/install-nginx.sh | bash`)
cmd.Env = os.Environ()
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
log.Printf("[Manage] Nginx installation failed: %v, stderr: %s", err, stderr.String())
return
}
log.Printf("[Manage] Nginx installed successfully")
if req.Domain != "" {
deployNginxSSLConfig(req.Domain)
}
}()
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Nginx installation started, please check status later",
})
}
// 处理 POST /api/child/nginx/remove。
func (h *ManageHandler) HandleNginxRemove(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
log.Printf("[Manage] Removing Nginx...")
cmd := exec.Command("bash", "-c",
`curl -fsSL https://raw.githubusercontent.com/iluobei/miaomiaowuX/main/uninstall-nginx.sh | bash -s -- -y`)
cmd.Env = os.Environ()
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
log.Printf("[Manage] Nginx removal failed: %v, stderr: %s", err, stderr.String())
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Removal failed: %v", err))
return
}
log.Printf("[Manage] Nginx removed successfully")
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Nginx removed successfully",
})
}
// ================== Nginx 配置 ==================
// 处理 GET/POST /api/child/nginx/config。
func (h *ManageHandler) HandleNginxConfig(w http.ResponseWriter, r *http.Request) {
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
switch r.Method {
case http.MethodGet:
h.getNginxConfig(w, r)
case http.MethodPost:
h.setNginxConfig(w, r)
default:
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
}
}
func (h *ManageHandler) getNginxConfig(w http.ResponseWriter, r *http.Request) {
configPaths := constants.DefaultNginxConfigPaths
var configPath string
var content []byte
var err error
for _, p := range configPaths {
content, err = os.ReadFile(p)
if err == nil {
configPath = p
break
}
}
if configPath == "" {
writeError(w, http.StatusNotFound, "Nginx config not found")
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"path": configPath,
"config": string(content),
})
}
func (h *ManageHandler) setNginxConfig(w http.ResponseWriter, r *http.Request) {
var req struct {
Config string `json:"config"`
Path string `json:"path,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid request body")
return
}
configPath := req.Path
if configPath == "" {
configPath = constants.DefaultNginxConfigPaths[0]
}
backupPath := configPath + ".bak." + time.Now().Format("20060102150405")
if content, err := os.ReadFile(configPath); err == nil {
os.WriteFile(backupPath, content, 0644)
}
if err := os.WriteFile(configPath, []byte(req.Config), 0644); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to write config: %v", err))
return
}
cmd := exec.Command("nginx", "-t")
output, err := cmd.CombinedOutput()
if err != nil {
if backup, err := os.ReadFile(backupPath); err == nil {
os.WriteFile(configPath, backup, 0644)
}
writeError(w, http.StatusBadRequest, fmt.Sprintf("Invalid nginx config: %s", string(output)))
return
}
log.Printf("[Manage] Nginx config saved to %s", configPath)
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Config saved successfully",
"path": configPath,
})
}
// ================== 系统信息 ==================
// 处理 GET /api/child/system/info。
func (h *ManageHandler) HandleSystemInfo(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
info := map[string]interface{}{
"success": true,
}
if hostname, err := os.Hostname(); err == nil {
info["hostname"] = hostname
}
if data, err := os.ReadFile("/proc/uptime"); err == nil {
parts := strings.Fields(string(data))
if len(parts) > 0 {
info["uptime"] = parts[0]
}
}
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
memInfo := make(map[string]string)
lines := strings.Split(string(data), "\n")
for _, line := range lines {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
if key == "MemTotal" || key == "MemFree" || key == "MemAvailable" {
memInfo[key] = value
}
}
}
info["memory"] = memInfo
}
if data, err := os.ReadFile("/proc/loadavg"); err == nil {
info["loadavg"] = strings.TrimSpace(string(data))
}
writeJSON(w, http.StatusOK, info)
}
// ================== 配置文件管理 ==================
// ConfigFileInfo 表示配置文件条目。
type ConfigFileInfo struct {
Name string `json:"name"`
Path string `json:"path"`
Size int64 `json:"size"`
ModTime string `json:"mod_time"`
}
// 处理 xray 配置文件的列表与读写。
func (h *ManageHandler) HandleXrayConfigFiles(w http.ResponseWriter, r *http.Request) {
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
switch r.Method {
case http.MethodGet:
file := r.URL.Query().Get("file")
if file != "" {
h.getXrayConfigFile(w, r, file)
} else {
h.listXrayConfigFiles(w, r)
}
case http.MethodPut, http.MethodPost:
h.saveXrayConfigFile(w, r)
default:
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
}
}
func (h *ManageHandler) listXrayConfigFiles(w http.ResponseWriter, r *http.Request) {
configDirs := constants.XrayConfigDirPaths
var files []ConfigFileInfo
var baseDir string
for _, dir := range configDirs {
if _, err := os.Stat(dir); err == nil {
baseDir = dir
entries, err := os.ReadDir(dir)
if err != nil {
continue
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
if !strings.HasSuffix(entry.Name(), ".json") {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
files = append(files, ConfigFileInfo{
Name: entry.Name(),
Path: filepath.Join(dir, entry.Name()),
Size: info.Size(),
ModTime: info.ModTime().Format(time.RFC3339),
})
}
break
}
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"base_dir": baseDir,
"files": files,
})
}
func (h *ManageHandler) getXrayConfigFile(w http.ResponseWriter, r *http.Request, file string) {
file = filepath.Clean(file)
configDirs := []string{
constants.XrayConfigDirPaths[0],
constants.XrayConfigDirPaths[1],
constants.XrayConfigDirPaths[2],
}
var filePath string
for _, dir := range configDirs {
candidate := filepath.Join(dir, file)
if !strings.HasPrefix(candidate, dir) {
continue
}
if _, err := os.Stat(candidate); err == nil {
filePath = candidate
break
}
}
if filePath == "" {
writeError(w, http.StatusNotFound, "File not found")
return
}
content, err := os.ReadFile(filePath)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read file: %v", err))
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"path": filePath,
"content": string(content),
})
}
func (h *ManageHandler) saveXrayConfigFile(w http.ResponseWriter, r *http.Request) {
var req struct {
File string `json:"file"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid request body")
return
}
if req.File == "" {
writeError(w, http.StatusBadRequest, "File name required")
return
}
req.File = filepath.Base(req.File)
if !strings.HasSuffix(req.File, ".json") {
req.File += ".json"
}
configDirs := []string{
constants.XrayConfigDirPaths[0],
constants.XrayConfigDirPaths[1],
constants.XrayConfigDirPaths[2],
}
var configDir string
for _, dir := range configDirs {
if _, err := os.Stat(dir); err == nil {
configDir = dir
break
}
}
if configDir == "" {
configDir = constants.XrayConfigDirPaths[0]
if err := os.MkdirAll(configDir, 0755); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create config directory: %v", err))
return
}
}
filePath := filepath.Join(configDir, req.File)
if strings.HasSuffix(req.File, ".json") {
var js json.RawMessage
if err := json.Unmarshal([]byte(req.Content), &js); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON content")
return
}
}
if err := os.WriteFile(filePath, []byte(req.Content), 0644); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to write file: %v", err))
return
}
log.Printf("[Manage] Xray config file saved: %s", filePath)
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "File saved successfully",
"path": filePath,
})
}
// 处理 nginx 配置文件的列表与读写。
func (h *ManageHandler) HandleNginxConfigFiles(w http.ResponseWriter, r *http.Request) {
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
switch r.Method {
case http.MethodGet:
file := r.URL.Query().Get("file")
if file != "" {
h.getNginxConfigFile(w, r, file)
} else {
h.listNginxConfigFiles(w, r)
}
case http.MethodPut, http.MethodPost:
h.saveNginxConfigFile(w, r)
default:
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
}
}
func (h *ManageHandler) listNginxConfigFiles(w http.ResponseWriter, r *http.Request) {
configDirs := []struct {
dir string
description string
}{
{constants.NginxConfigDirPaths[0], "main"},
{constants.NginxConfigDirPaths[1], "sites-available"},
{constants.NginxConfigDirPaths[2], "sites-enabled"},
{constants.NginxConfigDirPaths[3], "conf.d"},
}
result := make(map[string][]ConfigFileInfo)
for _, cd := range configDirs {
if _, err := os.Stat(cd.dir); err != nil {
continue
}
entries, err := os.ReadDir(cd.dir)
if err != nil {
continue
}
var files []ConfigFileInfo
for _, entry := range entries {
if entry.IsDir() {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
files = append(files, ConfigFileInfo{
Name: entry.Name(),
Path: filepath.Join(cd.dir, entry.Name()),
Size: info.Size(),
ModTime: info.ModTime().Format(time.RFC3339),
})
}
result[cd.description] = files
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"files": result,
})
}
func (h *ManageHandler) getNginxConfigFile(w http.ResponseWriter, r *http.Request, file string) {
file = filepath.Clean(file)
allowedDirs := []string{
constants.NginxConfigDirPaths[0],
constants.NginxConfigDirPaths[1],
constants.NginxConfigDirPaths[2],
constants.NginxConfigDirPaths[3],
constants.NginxConfigDirPaths[4],
}
var filePath string
if filepath.IsAbs(file) {
for _, dir := range allowedDirs {
if strings.HasPrefix(file, dir) {
if _, err := os.Stat(file); err == nil {
filePath = file
break
}
}
}
} else {
for _, dir := range allowedDirs {
candidate := filepath.Join(dir, file)
if _, err := os.Stat(candidate); err == nil {
filePath = candidate
break
}
}
}
if filePath == "" {
writeError(w, http.StatusNotFound, "File not found")
return
}
content, err := os.ReadFile(filePath)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read file: %v", err))
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"path": filePath,
"content": string(content),
})
}
func (h *ManageHandler) saveNginxConfigFile(w http.ResponseWriter, r *http.Request) {
var req struct {
Path string `json:"path"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid request body")
return
}
if req.Path == "" {
writeError(w, http.StatusBadRequest, "File path required")
return
}
req.Path = filepath.Clean(req.Path)
allowedDirs := []string{
constants.NginxConfigDirPaths[0],
constants.NginxConfigDirPaths[4],
}
allowed := false
for _, dir := range allowedDirs {
if strings.HasPrefix(req.Path, dir) {
allowed = true
break
}
}
if !allowed {
writeError(w, http.StatusForbidden, "Path not allowed")
return
}
if _, err := os.Stat(req.Path); err == nil {
backupPath := req.Path + ".bak." + time.Now().Format("20060102150405")
if content, err := os.ReadFile(req.Path); err == nil {
os.WriteFile(backupPath, content, 0644)
}
}
dir := filepath.Dir(req.Path)
if err := os.MkdirAll(dir, 0755); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create directory: %v", err))
return
}
if err := os.WriteFile(req.Path, []byte(req.Content), 0644); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to write file: %v", err))
return
}
cmd := exec.Command("nginx", "-t")
output, err := cmd.CombinedOutput()
if err != nil {
backupPath := req.Path + ".bak." + time.Now().Format("20060102150405")[:14]
if backup, err := os.ReadFile(backupPath); err == nil {
os.WriteFile(req.Path, backup, 0644)
}
writeError(w, http.StatusBadRequest, fmt.Sprintf("Invalid nginx config: %s", string(output)))
return
}
log.Printf("[Manage] Nginx config file saved: %s", req.Path)
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "File saved successfully",
"path": req.Path,
})
}
// ================== Xray 入站管理 ==================
// InboundRequest 表示入站管理请求。
type InboundRequest struct {
Action string `json:"action"`
Inbound map[string]interface{} `json:"inbound,omitempty"`
Tag string `json:"tag,omitempty"`
}
// 处理入站管理请求。
func (h *ManageHandler) HandleInbounds(w http.ResponseWriter, r *http.Request) {
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
switch r.Method {
case http.MethodGet:
h.listInbounds(w, r)
case http.MethodPost:
h.manageInbound(w, r)
default:
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
}
}
func (h *ManageHandler) listInbounds(w http.ResponseWriter, r *http.Request) {
configInbounds := h.getInboundsFromConfig()
runtimeTags := h.getInboundTagsFromGRPC()
mergedInbounds := h.mergeInbounds(configInbounds, runtimeTags)
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"inbounds": mergedInbounds,
})
}
func (h *ManageHandler) getInboundsFromConfig() []map[string]interface{} {
configPath := h.findXrayConfigPath()
if configPath == "" {
return nil
}
content, err := os.ReadFile(configPath)
if err != nil {
log.Printf("[Manage] Failed to read config file: %v", err)
return nil
}
var config map[string]interface{}
if err := json.Unmarshal(content, &config); err != nil {
log.Printf("[Manage] Failed to parse config: %v", err)
return nil
}
rawInbounds, _ := config["inbounds"].([]interface{})
inbounds := make([]map[string]interface{}, 0, len(rawInbounds))
for _, ib := range rawInbounds {
if ibMap, ok := ib.(map[string]interface{}); ok {
inbounds = append(inbounds, ibMap)
}
}
return inbounds
}
func (h *ManageHandler) getInboundTagsFromGRPC() []string {
apiPort := h.findXrayAPIPort()
if apiPort == 0 {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
clients, err := xrpc.New(ctx, constants.LocalhostIP, uint16(apiPort))
if err != nil {
log.Printf("[Manage] Failed to connect to Xray gRPC: %v", err)
return nil
}
defer clients.Connection.Close()
resp, err := clients.Handler.ListInbounds(ctx, &command.ListInboundsRequest{IsOnlyTags: true})
if err != nil {
log.Printf("[Manage] Failed to list inbounds via gRPC: %v", err)
return nil
}
tags := make([]string, 0, len(resp.Inbounds))
for _, ib := range resp.Inbounds {
// 过滤掉 tag="api" 和空 tagXray 内部入站)
if ib.Tag != "" && ib.Tag != "api" {
tags = append(tags, ib.Tag)
}
}
return tags
}
func (h *ManageHandler) mergeInbounds(configInbounds []map[string]interface{}, runtimeTags []string) []map[string]interface{} {
runtimeTagSet := make(map[string]bool)
for _, tag := range runtimeTags {
runtimeTagSet[tag] = true
}
configTagSet := make(map[string]bool)
for _, ib := range configInbounds {
if tag, ok := ib["tag"].(string); ok {
configTagSet[tag] = true
}
}
result := make([]map[string]interface{}, 0, len(configInbounds)+len(runtimeTags))
for _, ib := range configInbounds {
tag, _ := ib["tag"].(string)
// 跳过 tag="api" 的入站(Xray 内部 API 入站)
if tag == "api" {
continue
}
ibCopy := make(map[string]interface{})
for k, v := range ib {
ibCopy[k] = v
}
// 如果 tag 为空,根据协议和端口生成名称
if tag == "" {
protocol, _ := ib["protocol"].(string)
port := 0
if p, ok := ib["port"].(float64); ok {
port = int(p)
} else if p, ok := ib["port"].(int); ok {
port = p
}
if protocol != "" && port > 0 {
ibCopy["tag"] = fmt.Sprintf("%s-%d", protocol, port)
ibCopy["_generated_tag"] = true
}
}
if runtimeTagSet[tag] {
ibCopy["_runtime_status"] = "running"
} else {
ibCopy["_runtime_status"] = "not_running"
}
ibCopy["_source"] = "config"
result = append(result, ibCopy)
}
for _, tag := range runtimeTags {
if !configTagSet[tag] {
result = append(result, map[string]interface{}{
"tag": tag,
"_runtime_status": "running",
"_source": "runtime_only",
"_warning": "This inbound is not persisted and will be lost on restart",
})
}
}
return result
}
func (h *ManageHandler) manageInbound(w http.ResponseWriter, r *http.Request) {
var req InboundRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid request body")
return
}
action := strings.ToLower(strings.TrimSpace(req.Action))
if action == "" {
action = "add"
}
apiPort := h.findXrayAPIPort()
if apiPort == 0 {
writeError(w, http.StatusInternalServerError, "Xray API not available")
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
clients, err := xrpc.New(ctx, constants.LocalhostIP, uint16(apiPort))
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to connect to Xray: %v", err))
return
}
defer clients.Connection.Close()
switch action {
case "add":
if req.Inbound == nil {
writeError(w, http.StatusBadRequest, "Inbound payload is required")
return
}
if err := h.addInbound(ctx, clients.Handler, req.Inbound); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add inbound: %v", err))
return
}
if err := h.persistInbound(req.Inbound); err != nil {
log.Printf("[Manage] Error: Failed to persist inbound to config: %v", err)
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Inbound added to runtime, but failed to persist to config: " + err.Error(),
"warning": "persist_failed",
})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Inbound added successfully",
})
case "remove":
if req.Tag == "" {
writeError(w, http.StatusBadRequest, "Tag is required for remove action")
return
}
// 尝试从运行态移除(未运行时报错可忽略)
runtimeErr := h.removeInbound(ctx, clients.Handler, req.Tag)
if runtimeErr != nil {
log.Printf("[Manage] Warning: Failed to remove inbound from runtime: %v", runtimeErr)
}
// 从配置文件移除(主流程)
configErr := h.removeInboundFromConfig(req.Tag)
if configErr != nil {
log.Printf("[Manage] Warning: Failed to remove inbound from config: %v", configErr)
}
// 配置文件操作成功即可视为成功(运行态移除可选)
// 配置改动后若未重启,运行态可能还没有该入站
if configErr != nil {
// 配置文件操作失败
if runtimeErr != nil {
// 两边都失败时,判断是否只是“未找到”错误
if strings.Contains(runtimeErr.Error(), "not enough information") {
// Xray 返回运行态不存在该入站,这属于可接受情况
// 仅返回配置文件错误
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to remove inbound from config: %v", configErr))
} else {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to remove inbound: runtime=%v, config=%v", runtimeErr, configErr))
}
} else {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to remove inbound from config: %v", configErr))
}
return
}
// 配置成功时,运行态报错可接受(可能尚未加载)
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Inbound removed successfully",
})
default:
writeError(w, http.StatusBadRequest, "Invalid action. Must be 'add' or 'remove'")
}
}
// ================== Xray 出站管理 ==================
// OutboundRequest 表示出站管理请求。
type OutboundRequest struct {
Action string `json:"action"`
Outbound map[string]interface{} `json:"outbound,omitempty"`
Tag string `json:"tag,omitempty"`
}
// 处理出站管理请求。
func (h *ManageHandler) HandleOutbounds(w http.ResponseWriter, r *http.Request) {
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
switch r.Method {
case http.MethodGet:
h.listOutbounds(w, r)
case http.MethodPost:
h.manageOutbound(w, r)
default:
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
}
}
func (h *ManageHandler) listOutbounds(w http.ResponseWriter, r *http.Request) {
configPath := h.findXrayConfigPath()
if configPath == "" {
writeError(w, http.StatusNotFound, "Xray config not found")
return
}
content, err := os.ReadFile(configPath)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read config: %v", err))
return
}
var config map[string]interface{}
if err := json.Unmarshal(content, &config); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to parse config: %v", err))
return
}
outbounds, _ := config["outbounds"].([]interface{})
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"outbounds": outbounds,
})
}
func (h *ManageHandler) manageOutbound(w http.ResponseWriter, r *http.Request) {
var req OutboundRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid request body")
return
}
action := strings.ToLower(strings.TrimSpace(req.Action))
if action == "" {
action = "add"
}
apiPort := h.findXrayAPIPort()
if apiPort == 0 {
writeError(w, http.StatusInternalServerError, "Xray API not available")
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
clients, err := xrpc.New(ctx, constants.LocalhostIP, uint16(apiPort))
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to connect to Xray: %v", err))
return
}
defer clients.Connection.Close()
switch action {
case "add":
if req.Outbound == nil {
writeError(w, http.StatusBadRequest, "Outbound payload is required")
return
}
if err := h.addOutbound(ctx, clients.Handler, req.Outbound); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add outbound: %v", err))
return
}
if err := h.persistOutbound(req.Outbound); err != nil {
log.Printf("[Manage] Warning: Failed to persist outbound to config: %v", err)
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Outbound added successfully",
})
case "remove":
if req.Tag == "" {
writeError(w, http.StatusBadRequest, "Tag is required for remove action")
return
}
if err := h.removeOutbound(ctx, clients.Handler, req.Tag); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to remove outbound: %v", err))
return
}
if err := h.removeOutboundFromConfig(req.Tag); err != nil {
log.Printf("[Manage] Warning: Failed to remove outbound from config: %v", err)
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Outbound removed successfully",
})
default:
writeError(w, http.StatusBadRequest, "Invalid action. Must be 'add' or 'remove'")
}
}
// ================== Xray 路由管理 ==================
// RoutingRequest 表示路由管理请求。
type RoutingRequest struct {
Action string `json:"action"`
Routing map[string]interface{} `json:"routing,omitempty"`
Rule map[string]interface{} `json:"rule,omitempty"`
Index int `json:"index,omitempty"`
}
// 处理路由管理请求。
func (h *ManageHandler) HandleRouting(w http.ResponseWriter, r *http.Request) {
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
switch r.Method {
case http.MethodGet:
h.getRouting(w, r)
case http.MethodPost:
h.manageRouting(w, r)
default:
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
}
}
func (h *ManageHandler) getRouting(w http.ResponseWriter, r *http.Request) {
configPath := h.findXrayConfigPath()
if configPath == "" {
writeError(w, http.StatusNotFound, "Xray config not found")
return
}
content, err := os.ReadFile(configPath)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read config: %v", err))
return
}
var config map[string]interface{}
if err := json.Unmarshal(content, &config); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to parse config: %v", err))
return
}
routing, _ := config["routing"].(map[string]interface{})
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"routing": routing,
})
}
func (h *ManageHandler) manageRouting(w http.ResponseWriter, r *http.Request) {
var req RoutingRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid request body")
return
}
action := strings.ToLower(strings.TrimSpace(req.Action))
if action == "" {
action = "set"
}
configPath := h.findXrayConfigPath()
if configPath == "" {
writeError(w, http.StatusNotFound, "Xray config not found")
return
}
content, err := os.ReadFile(configPath)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read config: %v", err))
return
}
var config map[string]interface{}
if err := json.Unmarshal(content, &config); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to parse config: %v", err))
return
}
switch action {
case "set":
if req.Routing == nil {
writeError(w, http.StatusBadRequest, "Routing config is required")
return
}
config["routing"] = req.Routing
case "add_rule":
if req.Rule == nil {
writeError(w, http.StatusBadRequest, "Rule is required")
return
}
routing, _ := config["routing"].(map[string]interface{})
if routing == nil {
routing = map[string]interface{}{}
}
rules, _ := routing["rules"].([]interface{})
rules = append(rules, req.Rule)
routing["rules"] = rules
config["routing"] = routing
case "remove_rule":
routing, _ := config["routing"].(map[string]interface{})
if routing == nil {
writeError(w, http.StatusBadRequest, "No routing config found")
return
}
rules, _ := routing["rules"].([]interface{})
if req.Index < 0 || req.Index >= len(rules) {
writeError(w, http.StatusBadRequest, "Invalid rule index")
return
}
rules = append(rules[:req.Index], rules[req.Index+1:]...)
routing["rules"] = rules
config["routing"] = routing
default:
writeError(w, http.StatusBadRequest, "Invalid action")
return
}
newContent, err := json.MarshalIndent(config, "", " ")
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to marshal config: %v", err))
return
}
if err := os.WriteFile(configPath, newContent, 0644); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to write config: %v", err))
return
}
log.Printf("[Manage] Routing config updated")
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Routing updated successfully. Restart Xray to apply changes.",
})
}
// ================== 辅助函数 ==================
func (h *ManageHandler) findXrayConfigPath() string {
configPaths := constants.DefaultXrayConfigPaths
for _, p := range configPaths {
if _, err := os.Stat(p); err == nil {
return p
}
}
return ""
}
func (h *ManageHandler) findXrayAPIPort() int {
configPath := h.findXrayConfigPath()
if configPath == "" {
return 0
}
content, err := os.ReadFile(configPath)
if err != nil {
return 0
}
var config map[string]interface{}
if err := json.Unmarshal(content, &config); err != nil {
return 0
}
inbounds, ok := config["inbounds"].([]interface{})
if !ok {
return 0
}
for _, ib := range inbounds {
inbound, ok := ib.(map[string]interface{})
if !ok {
continue
}
tag, _ := inbound["tag"].(string)
if tag == "api" {
port, ok := inbound["port"].(float64)
if ok {
return int(port)
}
}
}
return 10085
}
func (h *ManageHandler) addInbound(ctx context.Context, handlerClient command.HandlerServiceClient, inbound map[string]interface{}) error {
inboundJSON, err := json.Marshal(inbound)
if err != nil {
return fmt.Errorf("failed to marshal inbound: %w", err)
}
inboundConfig := &conf.InboundDetourConfig{}
if err := json.Unmarshal(inboundJSON, inboundConfig); err != nil {
return fmt.Errorf("failed to unmarshal inbound config: %w", err)
}
rawConfig, err := inboundConfig.Build()
if err != nil {
return fmt.Errorf("failed to build inbound config: %w", err)
}
if tag, ok := inbound["tag"].(string); ok && tag != "" {
_, _ = handlerClient.RemoveInbound(ctx, &command.RemoveInboundRequest{
Tag: tag,
})
}
_, err = handlerClient.AddInbound(ctx, &command.AddInboundRequest{
Inbound: rawConfig,
})
return err
}
func (h *ManageHandler) removeInbound(ctx context.Context, handlerClient command.HandlerServiceClient, tag string) error {
_, err := handlerClient.RemoveInbound(ctx, &command.RemoveInboundRequest{
Tag: tag,
})
return err
}
func (h *ManageHandler) addOutbound(ctx context.Context, handlerClient command.HandlerServiceClient, outbound map[string]interface{}) error {
outboundJSON, err := json.Marshal(outbound)
if err != nil {
return fmt.Errorf("failed to marshal outbound: %w", err)
}
outboundConfig := &conf.OutboundDetourConfig{}
if err := json.Unmarshal(outboundJSON, outboundConfig); err != nil {
return fmt.Errorf("failed to unmarshal outbound config: %w", err)
}
rawConfig, err := outboundConfig.Build()
if err != nil {
return fmt.Errorf("failed to build outbound config: %w", err)
}
_, err = handlerClient.AddOutbound(ctx, &command.AddOutboundRequest{
Outbound: rawConfig,
})
return err
}
func (h *ManageHandler) removeOutbound(ctx context.Context, handlerClient command.HandlerServiceClient, tag string) error {
_, err := handlerClient.RemoveOutbound(ctx, &command.RemoveOutboundRequest{
Tag: tag,
})
return err
}
func (h *ManageHandler) persistInbound(inbound map[string]interface{}) error {
configPath := h.findXrayConfigPath()
if configPath == "" {
return fmt.Errorf("config file not found")
}
content, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}
var config map[string]interface{}
if err := json.Unmarshal(content, &config); err != nil {
return fmt.Errorf("failed to parse config: %w", err)
}
inbounds, _ := config["inbounds"].([]interface{})
inbounds = append(inbounds, inbound)
config["inbounds"] = inbounds
newContent, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
return os.WriteFile(configPath, newContent, 0644)
}
func (h *ManageHandler) removeInboundFromConfig(tag string) error {
configPath := h.findXrayConfigPath()
if configPath == "" {
return fmt.Errorf("config file not found")
}
content, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}
var config map[string]interface{}
if err := json.Unmarshal(content, &config); err != nil {
return fmt.Errorf("failed to parse config: %w", err)
}
inbounds, _ := config["inbounds"].([]interface{})
var newInbounds []interface{}
for _, ib := range inbounds {
inbound, ok := ib.(map[string]interface{})
if !ok {
newInbounds = append(newInbounds, ib)
continue
}
ibTag, _ := inbound["tag"].(string)
if ibTag != tag {
newInbounds = append(newInbounds, ib)
}
}
config["inbounds"] = newInbounds
newContent, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
return os.WriteFile(configPath, newContent, 0644)
}
func (h *ManageHandler) persistOutbound(outbound map[string]interface{}) error {
configPath := h.findXrayConfigPath()
if configPath == "" {
return fmt.Errorf("config file not found")
}
content, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}
var config map[string]interface{}
if err := json.Unmarshal(content, &config); err != nil {
return fmt.Errorf("failed to parse config: %w", err)
}
outbounds, _ := config["outbounds"].([]interface{})
outbounds = append(outbounds, outbound)
config["outbounds"] = outbounds
newContent, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
return os.WriteFile(configPath, newContent, 0644)
}
func (h *ManageHandler) removeOutboundFromConfig(tag string) error {
configPath := h.findXrayConfigPath()
if configPath == "" {
return fmt.Errorf("config file not found")
}
content, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}
var config map[string]interface{}
if err := json.Unmarshal(content, &config); err != nil {
return fmt.Errorf("failed to parse config: %w", err)
}
outbounds, _ := config["outbounds"].([]interface{})
var newOutbounds []interface{}
for _, ob := range outbounds {
outbound, ok := ob.(map[string]interface{})
if !ok {
newOutbounds = append(newOutbounds, ob)
continue
}
obTag, _ := outbound["tag"].(string)
if obTag != tag {
newOutbounds = append(newOutbounds, ob)
}
}
config["outbounds"] = newOutbounds
newContent, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
return os.WriteFile(configPath, newContent, 0644)
}
// ================== 扫描 ==================
// ScanResponse 表示扫描接口响应。
type ScanResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
XrayRunning bool `json:"xray_running"`
XrayVersion string `json:"xray_version,omitempty"`
APIPort int `json:"api_port,omitempty"`
ConfigPath string `json:"config_path,omitempty"`
Inbounds []map[string]interface{} `json:"inbounds,omitempty"`
ConfigModified bool `json:"config_modified,omitempty"`
ConfigAddedSections []string `json:"config_added_sections,omitempty"`
}
// 处理 POST /api/child/scan。
func (h *ManageHandler) HandleScan(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
log.Printf("[Manage] Scanning for Xray process...")
configResult := h.EnsureXrayConfig()
response := ScanResponse{
Success: true,
Message: "Scan completed",
}
if configResult.Modified {
response.ConfigModified = true
response.ConfigAddedSections = configResult.AddedSections
log.Printf("[Manage] Xray config auto-completed, added sections: %v", configResult.AddedSections)
cmd := exec.Command("systemctl", "restart", "xray")
if err := cmd.Run(); err != nil {
log.Printf("[Manage] Failed to restart xray after config update: %v", err)
} else {
log.Printf("[Manage] Xray restarted after config update")
time.Sleep(1 * time.Second)
}
} else if configResult.Error != "" {
log.Printf("[Manage] Xray config check warning: %s", configResult.Error)
}
xrayStatus := h.getXrayStatus()
if xrayStatus != nil {
response.XrayRunning = xrayStatus.Running
response.XrayVersion = xrayStatus.Version
}
configPath := h.findXrayConfigPath()
if configPath != "" {
response.ConfigPath = configPath
response.APIPort = h.findXrayAPIPort()
content, err := os.ReadFile(configPath)
if err == nil {
var config map[string]interface{}
if json.Unmarshal(content, &config) == nil {
if inbounds, ok := config["inbounds"].([]interface{}); ok {
for _, ib := range inbounds {
if inbound, ok := ib.(map[string]interface{}); ok {
if tag, _ := inbound["tag"].(string); tag == "api" {
continue
}
response.Inbounds = append(response.Inbounds, inbound)
}
}
}
}
}
}
if response.XrayRunning {
response.Message = fmt.Sprintf("Xray is running, found %d inbound(s)", len(response.Inbounds))
if response.ConfigModified {
response.Message += fmt.Sprintf(", config updated: added %v", response.ConfigAddedSections)
}
} else if xrayStatus != nil && xrayStatus.Installed {
response.Message = "Xray is installed but not running"
} else {
response.Message = "Xray is not installed"
}
log.Printf("[Manage] Scan result: %s", response.Message)
writeJSON(w, http.StatusOK, response)
}
// ================== Xray 配置自动补全 ==================
// EnsureXrayConfigResult 表示配置检查结果。
type EnsureXrayConfigResult struct {
ConfigPath string `json:"config_path"`
Modified bool `json:"modified"`
AddedSections []string `json:"added_sections,omitempty"`
Error string `json:"error,omitempty"`
}
// 检查并补全 Xray 配置。
func (h *ManageHandler) EnsureXrayConfig() *EnsureXrayConfigResult {
result := &EnsureXrayConfigResult{}
configPath := h.findXrayConfigPath()
if configPath == "" {
result.Error = "Xray config not found"
return result
}
result.ConfigPath = configPath
content, err := os.ReadFile(configPath)
if err != nil {
result.Error = fmt.Sprintf("Failed to read config: %v", err)
return result
}
var config map[string]interface{}
if err := json.Unmarshal(content, &config); err != nil {
result.Error = fmt.Sprintf("Invalid JSON: %v", err)
return result
}
modified := false
if _, ok := config["api"]; !ok {
config["api"] = map[string]interface{}{
"tag": "api",
"services": []interface{}{"HandlerService", "LoggerService", "StatsService", "RoutingService"},
}
result.AddedSections = append(result.AddedSections, "api")
modified = true
}
if _, ok := config["stats"]; !ok {
config["stats"] = map[string]interface{}{}
result.AddedSections = append(result.AddedSections, "stats")
modified = true
}
if !h.hasValidPolicy(config) {
config["policy"] = h.getTemplatePolicy()
result.AddedSections = append(result.AddedSections, "policy")
modified = true
}
if !h.hasAPIInbound(config) {
h.addAPIInbound(config)
result.AddedSections = append(result.AddedSections, "api_inbound")
modified = true
}
if !h.hasAPIRoutingRule(config) {
h.addAPIRoutingRule(config)
result.AddedSections = append(result.AddedSections, "api_routing_rule")
modified = true
}
if modified {
backupPath := configPath + ".backup"
if err := os.WriteFile(backupPath, content, 0644); err != nil {
log.Printf("[Manage] Warning: failed to backup config: %v", err)
}
newContent, _ := json.MarshalIndent(config, "", " ")
if err := os.WriteFile(configPath, newContent, 0644); err != nil {
result.Error = fmt.Sprintf("Failed to write config: %v", err)
return result
}
result.Modified = true
log.Printf("[Manage] Xray config updated, added: %v", result.AddedSections)
}
return result
}
func (h *ManageHandler) hasValidPolicy(config map[string]interface{}) bool {
policy, ok := config["policy"].(map[string]interface{})
if !ok {
return false
}
levels, ok := policy["levels"].(map[string]interface{})
if !ok {
return false
}
level0, ok := levels["0"].(map[string]interface{})
if !ok {
return false
}
statsUplink, _ := level0["statsUserUplink"].(bool)
statsDownlink, _ := level0["statsUserDownlink"].(bool)
return statsUplink && statsDownlink
}
func (h *ManageHandler) getTemplatePolicy() map[string]interface{} {
return map[string]interface{}{
"levels": map[string]interface{}{
"0": map[string]interface{}{
"handshake": float64(5),
"connIdle": float64(300),
"uplinkOnly": float64(2),
"downlinkOnly": float64(2),
"statsUserUplink": true,
"statsUserDownlink": true,
},
},
"system": map[string]interface{}{
"statsInboundUplink": true,
"statsInboundDownlink": true,
"statsOutboundUplink": true,
"statsOutboundDownlink": true,
},
}
}
func (h *ManageHandler) hasAPIInbound(config map[string]interface{}) bool {
inbounds, ok := config["inbounds"].([]interface{})
if !ok {
return false
}
for _, ib := range inbounds {
if inbound, ok := ib.(map[string]interface{}); ok {
if tag, _ := inbound["tag"].(string); tag == "api" {
return true
}
}
}
return false
}
func (h *ManageHandler) addAPIInbound(config map[string]interface{}) {
apiInbound := map[string]interface{}{
"tag": "api",
"port": float64(46736),
"listen": constants.LocalhostIP,
"protocol": "dokodemo-door",
"settings": map[string]interface{}{
"address": constants.LocalhostIP,
},
}
inbounds, ok := config["inbounds"].([]interface{})
if !ok {
inbounds = []interface{}{}
}
config["inbounds"] = append([]interface{}{apiInbound}, inbounds...)
}
func (h *ManageHandler) hasAPIRoutingRule(config map[string]interface{}) bool {
routing, ok := config["routing"].(map[string]interface{})
if !ok {
return false
}
rules, ok := routing["rules"].([]interface{})
if !ok {
return false
}
for _, r := range rules {
if rule, ok := r.(map[string]interface{}); ok {
if outboundTag, _ := rule["outboundTag"].(string); outboundTag == "api" {
return true
}
}
}
return false
}
func (h *ManageHandler) addAPIRoutingRule(config map[string]interface{}) {
apiRule := map[string]interface{}{
"type": "field",
"inboundTag": []interface{}{"api"},
"outboundTag": "api",
}
routing, ok := config["routing"].(map[string]interface{})
if !ok {
routing = map[string]interface{}{
"domainStrategy": "IPIfNonMatch",
"rules": []interface{}{},
}
config["routing"] = routing
}
rules, ok := routing["rules"].([]interface{})
if !ok {
rules = []interface{}{}
}
routing["rules"] = append([]interface{}{apiRule}, rules...)
}
// ================== 证书部署 ==================
// CertDeployRequest 表示主控端下发的证书部署请求。
type CertDeployRequest struct {
Domain string `json:"domain"`
CertPEM string `json:"cert_pem"`
KeyPEM string `json:"key_pem"`
CertPath string `json:"cert_path"`
KeyPath string `json:"key_path"`
Reload string `json:"reload"` // nginx, xray, both, none
}
// 处理 POST /api/child/cert/deploy。
func (h *ManageHandler) HandleCertDeploy(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "unauthorized")
return
}
var req CertDeployRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.CertPEM == "" || req.KeyPEM == "" || req.CertPath == "" || req.KeyPath == "" {
writeError(w, http.StatusBadRequest, "cert_pem, key_pem, cert_path, key_path are required")
return
}
if err := deployCertFiles(req.CertPEM, req.KeyPEM, req.CertPath, req.KeyPath, req.Reload); err != nil {
log.Printf("[CertDeploy] Failed to deploy cert for %s: %v", req.Domain, err)
writeError(w, http.StatusInternalServerError, fmt.Sprintf("deploy failed: %v", err))
return
}
log.Printf("[CertDeploy] Successfully deployed cert for %s to %s", req.Domain, req.CertPath)
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": fmt.Sprintf("certificate for %s deployed", req.Domain),
})
}
func deployCertFiles(certPEM, keyPEM, certPath, keyPath, reloadTarget string) error {
if err := os.MkdirAll(filepath.Dir(certPath), 0755); err != nil {
return fmt.Errorf("create cert dir: %w", err)
}
if err := os.MkdirAll(filepath.Dir(keyPath), 0755); err != nil {
return fmt.Errorf("create key dir: %w", err)
}
if err := os.WriteFile(certPath, []byte(certPEM), 0644); err != nil {
return fmt.Errorf("write cert: %w", err)
}
if err := os.WriteFile(keyPath, []byte(keyPEM), 0600); err != nil {
return fmt.Errorf("write key: %w", err)
}
switch reloadTarget {
case "nginx":
return reloadNginx()
case "xray":
return runCommand("systemctl", "restart", "xray")
case "both":
if err := reloadNginx(); err != nil {
return err
}
return runCommand("systemctl", "restart", "xray")
}
return nil
}
func deployNginxSSLConfig(domain string) {
domain = strings.TrimSpace(domain)
if domain == "" {
return
}
confDir := constants.NginxConfigDirPaths[4]
if _, err := os.Stat(confDir); err != nil {
confDir = constants.NginxConfigDirPaths[0]
}
certDir := filepath.Join(confDir, "cert")
os.MkdirAll(certDir, 0755)
serverBlock := fmt.Sprintf(`server {
listen 443 ssl default_server;
listen [::]:443 ssl;
http2 on;
server_name %s;
ssl_certificate cert/%s.pem;
ssl_certificate_key cert/%s.key;
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
location / {
root /usr/local/nginx/html;
index index.html;
}
}
`, domain, domain, domain)
// 写入 conf.d,或通过 include 挂载
confDDir := filepath.Join(confDir, "conf.d")
os.MkdirAll(confDDir, 0755)
sslConfPath := filepath.Join(confDDir, "ssl.conf")
if err := os.WriteFile(sslConfPath, []byte(serverBlock), 0644); err != nil {
log.Printf("[Manage] Failed to write nginx SSL config: %v", err)
return
}
// 确保主 nginx.conf 包含 conf.d/*.conf
mainConf := filepath.Join(confDir, "nginx.conf")
content, err := os.ReadFile(mainConf)
if err != nil {
log.Printf("[Manage] Failed to read nginx.conf: %v", err)
return
}
includeDirective := "include conf.d/*.conf;"
if !strings.Contains(string(content), includeDirective) {
// 在 http 块最后一个右括号前插入 include
text := string(content)
lastBrace := strings.LastIndex(text, "}")
if lastBrace > 0 {
text = text[:lastBrace] + " " + includeDirective + "\n" + text[lastBrace:]
if err := os.WriteFile(mainConf, []byte(text), 0644); err != nil {
log.Printf("[Manage] Failed to update nginx.conf with include: %v", err)
return
}
}
}
log.Printf("[Manage] Nginx SSL config deployed for domain %s at %s", domain, sslConfPath)
}
// HandleNginxSetupSSL 处理 POST /api/child/nginx/setup-ssl。
// 部署 nginx.conf 和 servers/{domain}.conf。
func (h *ManageHandler) HandleNginxSetupSSL(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
var req struct {
Domain string `json:"domain"`
NginxConfig string `json:"nginx_config"`
DomainConfig string `json:"domain_config"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Domain == "" {
writeError(w, http.StatusBadRequest, "domain is required")
return
}
domain := strings.ToLower(strings.TrimSpace(req.Domain))
confDir := constants.NginxPrimaryPrefixDir
if _, err := os.Stat(confDir); err != nil {
confDir = constants.NginxConfigDirPaths[0]
}
// 确保证书和 servers 目录存在
os.MkdirAll(filepath.Join(confDir, "cert"), 0755)
os.MkdirAll(filepath.Join(confDir, "servers"), 0755)
if req.NginxConfig != "" {
// 下发主 nginx.conf
mainConf := filepath.Join(confDir, "nginx.conf")
if content, err := os.ReadFile(mainConf); err == nil {
os.WriteFile(mainConf+".bak."+time.Now().Format("20060102150405"), content, 0644)
}
if err := os.WriteFile(mainConf, []byte(req.NginxConfig), 0644); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to write nginx.conf: %v", err))
return
}
log.Printf("[Manage] nginx.conf deployed at %s", mainConf)
}
if req.DomainConfig != "" {
// 下发域名 server 配置到 servers/{domain}.conf
domainConfPath := filepath.Join(confDir, "servers", domain+".conf")
if err := os.WriteFile(domainConfPath, []byte(req.DomainConfig), 0644); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to write domain config: %v", err))
return
}
log.Printf("[Manage] Domain config deployed at %s", domainConfPath)
} else {
// 兜底:沿用旧逻辑
deployNginxSSLConfig(domain)
}
// 重载 nginx 使配置生效
if err := reloadNginx(); err != nil {
log.Printf("[Manage] Nginx reload after setup-ssl failed: %v", err)
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": fmt.Sprintf("SSL config deployed for %s", domain),
})
}
func reloadNginx() error {
for _, bin := range constants.NginxBinarySearchPaths {
if path, err := exec.LookPath(bin); err == nil {
return runCommand(path, "-s", "reload")
}
}
return runCommand("systemctl", "reload", "nginx")
}
func runCommand(name string, args ...string) error {
if output, err := exec.Command(name, args...).CombinedOutput(); err != nil {
return fmt.Errorf("%s: %s: %w", name, string(output), err)
}
return nil
}
// 在缺失配置时下发内置默认配置。
func (h *ManageHandler) deployDefaultXrayConfig() {
configPath := constants.DefaultXrayConfigPaths[0]
if _, err := os.Stat(configPath); err == nil {
// 配置已存在,执行 EnsureXrayConfig 补齐缺失段
result := h.EnsureXrayConfig()
if result.Modified {
log.Printf("[Manage] Xray config updated after install: added %v", result.AddedSections)
exec.Command("systemctl", "restart", "xray").Run()
}
return
}
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
log.Printf("[Manage] Failed to create xray config dir: %v", err)
return
}
if err := os.WriteFile(configPath, defaultXrayConfig, 0644); err != nil {
log.Printf("[Manage] Failed to write default xray config: %v", err)
return
}
log.Printf("[Manage] Deployed default xray config to %s", configPath)
exec.Command("systemctl", "restart", "xray").Run()
}
// ================== SSE 流式安装/卸载 ==================
func sseStreamCmd(w http.ResponseWriter, r *http.Request, cmd *exec.Cmd, completeMsg string) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming not supported", http.StatusInternalServerError)
return
}
stdout, err := cmd.StdoutPipe()
if err != nil {
sseEvent(w, flusher, map[string]string{"type": "error", "message": err.Error()})
return
}
stderr, err := cmd.StderrPipe()
if err != nil {
sseEvent(w, flusher, map[string]string{"type": "error", "message": err.Error()})
return
}
if err := cmd.Start(); err != nil {
sseEvent(w, flusher, map[string]string{"type": "error", "message": err.Error()})
return
}
var mu sync.Mutex
var wg sync.WaitGroup
wg.Add(2)
scanStream := func(rc io.ReadCloser) {
defer wg.Done()
scanner := bufio.NewScanner(rc)
scanner.Buffer(make([]byte, 256*1024), 256*1024)
for scanner.Scan() {
select {
case <-r.Context().Done():
return
default:
}
mu.Lock()
sseEvent(w, flusher, map[string]string{"type": "output", "data": scanner.Text()})
mu.Unlock()
}
}
go scanStream(stdout)
go scanStream(stderr)
done := make(chan error, 1)
go func() { done <- cmd.Wait() }()
select {
case err := <-done:
wg.Wait()
if err != nil {
sseEvent(w, flusher, map[string]string{"type": "error", "message": err.Error()})
} else {
sseEvent(w, flusher, map[string]interface{}{"type": "complete", "success": true, "message": completeMsg})
}
case <-r.Context().Done():
cmd.Process.Kill()
sseEvent(w, flusher, map[string]string{"type": "error", "message": "request cancelled"})
}
}
func sseEvent(w http.ResponseWriter, flusher http.Flusher, data interface{}) {
b, _ := json.Marshal(data)
fmt.Fprintf(w, "data: %s\n\n", b)
flusher.Flush()
}
func (h *ManageHandler) HandleXrayInstallStream(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
log.Printf("[Manage] Starting Xray install (stream)...")
cmd := exec.CommandContext(r.Context(), "bash", "-c",
`bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install`)
cmd.Env = os.Environ()
sseStreamCmd(w, r, cmd, "Xray installed successfully")
// 安装完成后下发默认配置
h.deployDefaultXrayConfig()
}
func (h *ManageHandler) HandleXrayRemoveStream(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
log.Printf("[Manage] Starting Xray remove (stream)...")
cmd := exec.CommandContext(r.Context(), "bash", "-c",
`bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ remove`)
cmd.Env = os.Environ()
sseStreamCmd(w, r, cmd, "Xray removed successfully")
}
func (h *ManageHandler) HandleNginxInstallStream(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
if nginxInstalling.Load() {
writeError(w, http.StatusConflict, "Nginx installation already in progress")
return
}
nginxInstalling.Store(true)
defer nginxInstalling.Store(false)
domain := r.URL.Query().Get("domain")
log.Printf("[Manage] Starting Nginx install (stream)...")
cmd := exec.CommandContext(r.Context(), "bash", "-c",
`curl -fsSL https://raw.githubusercontent.com/iluobei/miaomiaowuX/main/install-nginx.sh | bash`)
cmd.Env = os.Environ()
sseStreamCmd(w, r, cmd, "Nginx installed successfully")
if domain != "" {
deployNginxSSLConfig(domain)
}
}
func (h *ManageHandler) HandleNginxRemoveStream(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
log.Printf("[Manage] Starting Nginx remove (stream)...")
cmd := exec.CommandContext(r.Context(), "bash", "-c",
`curl -fsSL https://raw.githubusercontent.com/iluobei/miaomiaowuX/main/uninstall-nginx.sh | bash -s -- -y`)
cmd.Env = os.Environ()
sseStreamCmd(w, r, cmd, "Nginx removed successfully")
}