Files
mmw-agent/internal/handler/management_handler.go
T

2822 lines
75 KiB
Go
Raw Normal View History

2026-01-28 13:13:58 +08:00
package handler
import (
2026-04-03 15:51:49 +08:00
"bufio"
2026-01-28 13:13:58 +08:00
"bytes"
"context"
"encoding/json"
"fmt"
2026-04-03 15:51:49 +08:00
"io"
2026-01-28 13:13:58 +08:00
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
2026-04-03 15:51:49 +08:00
"sync"
2026-04-02 23:23:38 +08:00
"sync/atomic"
2026-01-28 13:13:58 +08:00
"time"
2026-04-10 15:25:21 +08:00
"mmw-agent/internal/constants"
2026-01-28 13:13:58 +08:00
"mmw-agent/internal/xrpc"
"github.com/xtls/xray-core/app/proxyman/command"
"github.com/xtls/xray-core/infra/conf"
)
2026-04-02 23:23:38 +08:00
var nginxInstalling atomic.Bool
2026-04-10 15:25:21 +08:00
// ManageHandler 处理子端管理接口请求。
2026-01-28 13:13:58 +08:00
type ManageHandler struct {
configToken string
}
2026-04-10 15:25:21 +08:00
// 创建管理处理器。
2026-01-28 13:13:58 +08:00
func NewManageHandler(configToken string) *ManageHandler {
return &ManageHandler{
configToken: configToken,
}
}
2026-04-10 15:25:21 +08:00
// 校验请求身份(token + User-Agent)。
2026-01-28 13:13:58 +08:00
func (h *ManageHandler) authenticate(r *http.Request) bool {
2026-04-10 15:25:21 +08:00
if r.Header.Get(constants.HeaderUserAgent) != constants.AgentUserAgent {
2026-03-12 16:13:50 +08:00
return false
}
2026-01-28 13:13:58 +08:00
if h.configToken == "" {
return true
}
2026-04-10 15:25:21 +08:00
auth := r.Header.Get(constants.HeaderAuthorization)
2026-01-28 13:13:58 +08:00
if auth == "" {
2026-04-10 15:25:21 +08:00
auth = r.Header.Get(constants.HeaderMMRemoteToken)
2026-01-28 13:13:58 +08:00
}
if auth == "" {
return false
}
2026-04-10 15:25:21 +08:00
if strings.HasPrefix(auth, constants.BearerPrefix) {
token := strings.TrimPrefix(auth, constants.BearerPrefix)
2026-01-28 13:13:58 +08:00
return token == h.configToken
}
return auth == h.configToken
}
2026-04-10 15:25:21 +08:00
// 输出 JSON 响应。
2026-01-28 13:13:58 +08:00
func writeJSON(w http.ResponseWriter, statusCode int, data interface{}) {
2026-04-10 15:25:21 +08:00
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
2026-01-28 13:13:58 +08:00
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
2026-04-10 15:25:21 +08:00
// 输出错误响应。
2026-01-28 13:13:58 +08:00
func writeError(w http.ResponseWriter, statusCode int, message string) {
writeJSON(w, statusCode, map[string]interface{}{
"success": false,
"error": message,
})
}
2026-04-10 15:25:21 +08:00
// ================== 系统服务状态 ==================
2026-01-28 13:13:58 +08:00
2026-04-10 15:25:21 +08:00
// ServicesStatusResponse 表示服务状态查询响应。
2026-01-28 13:13:58 +08:00
type ServicesStatusResponse struct {
Success bool `json:"success"`
Xray *ServiceStatus `json:"xray,omitempty"`
Nginx *ServiceStatus `json:"nginx,omitempty"`
}
2026-04-10 15:25:21 +08:00
// ServiceStatus 表示单个服务状态。
2026-01-28 13:13:58 +08:00
type ServiceStatus struct {
Installed bool `json:"installed"`
Running bool `json:"running"`
Version string `json:"version,omitempty"`
}
2026-04-10 15:25:21 +08:00
// 处理 GET /api/child/services/status。
2026-01-28 13:13:58 +08:00
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 {
2026-04-10 15:25:21 +08:00
for _, p := range constants.XrayBinarySearchPaths {
2026-01-28 13:13:58 +08:00
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])
}
}
}
2026-04-10 15:25:21 +08:00
// 优先使用 systemctl 检查
2026-01-28 13:13:58 +08:00
cmd := exec.Command("systemctl", "is-active", "xray")
output, _ := cmd.Output()
if strings.TrimSpace(string(output)) == "active" {
status.Running = true
return status
}
2026-04-10 15:25:21 +08:00
// 兜底:用 pgrep 检查 xray 进程
2026-01-28 13:13:58 +08:00
pgrepCmd := exec.Command("pgrep", "-x", "xray")
if err := pgrepCmd.Run(); err == nil {
status.Running = true
return status
}
2026-04-10 15:25:21 +08:00
// 兜底:用 ps 检查包含 "xray" 的进程
2026-01-28 13:13:58 +08:00
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{}
2026-04-10 15:25:21 +08:00
// 先查 PATH,再查编译安装路径
2026-01-28 13:13:58 +08:00
nginxPath, err := exec.LookPath("nginx")
2026-04-02 23:23:38 +08:00
if err != nil {
2026-04-10 15:25:21 +08:00
for _, candidate := range constants.NginxBinarySearchPaths {
if strings.Contains(candidate, "/") {
if _, statErr := os.Stat(candidate); statErr == nil {
nginxPath = candidate
err = nil
break
}
}
2026-04-02 23:23:38 +08:00
}
}
2026-01-28 13:13:58 +08:00
if err == nil {
status.Installed = true
cmd := exec.Command(nginxPath, "-v")
output, err := cmd.CombinedOutput()
if err == nil {
status.Version = strings.TrimSpace(string(output))
}
}
2026-04-02 23:23:38 +08:00
if nginxInstalling.Load() {
status.Version = "安装中..."
}
2026-04-10 15:25:21 +08:00
// 优先使用 systemctl 检查
2026-01-28 13:13:58 +08:00
cmd := exec.Command("systemctl", "is-active", "nginx")
output, _ := cmd.Output()
if strings.TrimSpace(string(output)) == "active" {
status.Running = true
return status
}
2026-04-10 15:25:21 +08:00
// 兜底:用 pgrep 检查 nginx 进程
2026-01-28 13:13:58 +08:00
pgrepCmd := exec.Command("pgrep", "-x", "nginx")
if err := pgrepCmd.Run(); err == nil {
status.Running = true
return status
}
2026-04-10 15:25:21 +08:00
// 兜底:用 ps 检查 nginx master 进程
2026-01-28 13:13:58 +08:00
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
}
2026-04-10 15:25:21 +08:00
// ================== 服务控制 ==================
2026-01-28 13:13:58 +08:00
2026-04-10 15:25:21 +08:00
// ServiceControlRequest 表示服务控制请求。
2026-01-28 13:13:58 +08:00
type ServiceControlRequest struct {
Service string `json:"service"`
Action string `json:"action"`
}
2026-04-10 15:25:21 +08:00
// 处理 POST /api/child/services/control。
2026-01-28 13:13:58 +08:00
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),
})
}
2026-04-10 15:25:21 +08:00
// ================== Xray 安装 ==================
2026-01-28 13:13:58 +08:00
2026-04-10 15:25:21 +08:00
// 处理 POST /api/child/xray/install。
2026-01-28 13:13:58 +08:00
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")
2026-04-10 15:25:21 +08:00
// 若无配置则下发默认配置
2026-04-07 16:35:45 +08:00
h.deployDefaultXrayConfig()
2026-01-28 13:13:58 +08:00
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Xray installed successfully",
"output": stdout.String(),
})
}
2026-04-10 15:25:21 +08:00
// 处理 POST /api/child/xray/remove。
2026-01-28 13:13:58 +08:00
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(),
})
}
2026-04-10 15:25:21 +08:00
// ================== Xray 配置 ==================
2026-01-28 13:13:58 +08:00
2026-04-10 15:25:21 +08:00
// 处理 GET/POST /api/child/xray/config。
2026-01-28 13:13:58 +08:00
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) {
2026-04-10 15:25:21 +08:00
configPaths := constants.DefaultXrayConfigPaths
2026-01-28 13:13:58 +08:00
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 == "" {
2026-04-10 15:25:21 +08:00
configPath = constants.DefaultXrayConfigPaths[0]
2026-01-28 13:13:58 +08:00
}
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,
})
}
2026-04-10 15:25:21 +08:00
// ================== Xray 系统配置 ==================
2026-01-28 13:13:58 +08:00
2026-04-10 15:25:21 +08:00
// XraySystemConfig 表示 Xray 系统配置状态。
2026-01-28 13:13:58 +08:00
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"`
}
2026-04-10 15:25:21 +08:00
// 处理 GET/POST /api/child/xray/system-config。
2026-01-28 13:13:58 +08:00
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{
2026-04-10 15:25:21 +08:00
MetricsListen: constants.DefaultMetricsListen,
2026-01-28 13:13:58 +08:00
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),
2026-04-10 15:25:21 +08:00
"listen": constants.LocalhostIP,
2026-01-28 13:13:58 +08:00
"protocol": "dokodemo-door",
"settings": map[string]interface{}{
2026-04-10 15:25:21 +08:00
"address": constants.LocalhostIP,
2026-01-28 13:13:58 +08:00
},
}
config["inbounds"] = append([]interface{}{apiInbound}, inbounds...)
}
} else {
config["inbounds"] = []interface{}{
map[string]interface{}{
"tag": "api",
"port": float64(req.GrpcPort),
2026-04-10 15:25:21 +08:00
"listen": constants.LocalhostIP,
2026-01-28 13:13:58 +08:00
"protocol": "dokodemo-door",
"settings": map[string]interface{}{
2026-04-10 15:25:21 +08:00
"address": constants.LocalhostIP,
2026-01-28 13:13:58 +08:00
},
},
}
}
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
}
2026-04-10 15:25:21 +08:00
// ================== Nginx 安装 ==================
2026-01-28 13:13:58 +08:00
2026-04-10 15:25:21 +08:00
// 处理 POST /api/child/nginx/install。
2026-01-28 13:13:58 +08:00
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
}
2026-04-02 23:23:38 +08:00
if nginxInstalling.Load() {
writeError(w, http.StatusConflict, "Nginx installation already in progress")
2026-01-28 13:13:58 +08:00
return
}
2026-04-07 20:58:13 +08:00
var req struct {
Domain string `json:"domain"`
}
json.NewDecoder(r.Body).Decode(&req)
2026-04-02 23:23:38 +08:00
log.Printf("[Manage] Starting Nginx installation (async)...")
nginxInstalling.Store(true)
2026-01-28 13:13:58 +08:00
2026-04-02 23:23:38 +08:00
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()
2026-01-28 13:13:58 +08:00
2026-04-02 23:23:38 +08:00
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")
2026-04-07 20:58:13 +08:00
if req.Domain != "" {
deployNginxSSLConfig(req.Domain)
}
2026-04-02 23:23:38 +08:00
}()
2026-01-28 13:13:58 +08:00
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
2026-04-02 23:23:38 +08:00
"message": "Nginx installation started, please check status later",
2026-01-28 13:13:58 +08:00
})
}
2026-04-10 15:25:21 +08:00
// 处理 POST /api/child/nginx/remove。
2026-01-28 13:13:58 +08:00
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...")
2026-04-02 23:23:38 +08:00
cmd := exec.Command("bash", "-c",
`curl -fsSL https://raw.githubusercontent.com/iluobei/miaomiaowuX/main/uninstall-nginx.sh | bash -s -- -y`)
cmd.Env = os.Environ()
2026-01-28 13:13:58 +08:00
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",
})
}
2026-04-10 15:25:21 +08:00
// ================== Nginx 配置 ==================
2026-01-28 13:13:58 +08:00
2026-04-10 15:25:21 +08:00
// 处理 GET/POST /api/child/nginx/config。
2026-01-28 13:13:58 +08:00
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) {
2026-04-10 15:25:21 +08:00
configPaths := constants.DefaultNginxConfigPaths
2026-01-28 13:13:58 +08:00
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 == "" {
2026-04-10 15:25:21 +08:00
configPath = constants.DefaultNginxConfigPaths[0]
2026-01-28 13:13:58 +08:00
}
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,
})
}
2026-04-10 15:25:21 +08:00
// ================== 系统信息 ==================
2026-01-28 13:13:58 +08:00
2026-04-10 15:25:21 +08:00
// 处理 GET /api/child/system/info。
2026-01-28 13:13:58 +08:00
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)
}
2026-04-10 15:25:21 +08:00
// ================== 配置文件管理 ==================
2026-01-28 13:13:58 +08:00
2026-04-10 15:25:21 +08:00
// ConfigFileInfo 表示配置文件条目。
2026-01-28 13:13:58 +08:00
type ConfigFileInfo struct {
Name string `json:"name"`
Path string `json:"path"`
Size int64 `json:"size"`
ModTime string `json:"mod_time"`
}
2026-04-10 15:25:21 +08:00
// 处理 xray 配置文件的列表与读写。
2026-01-28 13:13:58 +08:00
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) {
2026-04-10 15:25:21 +08:00
configDirs := constants.XrayConfigDirPaths
2026-01-28 13:13:58 +08:00
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{
2026-04-10 15:25:21 +08:00
constants.XrayConfigDirPaths[0],
constants.XrayConfigDirPaths[1],
constants.XrayConfigDirPaths[2],
2026-01-28 13:13:58 +08:00
}
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{
2026-04-10 15:25:21 +08:00
constants.XrayConfigDirPaths[0],
constants.XrayConfigDirPaths[1],
constants.XrayConfigDirPaths[2],
2026-01-28 13:13:58 +08:00
}
var configDir string
for _, dir := range configDirs {
if _, err := os.Stat(dir); err == nil {
configDir = dir
break
}
}
if configDir == "" {
2026-04-10 15:25:21 +08:00
configDir = constants.XrayConfigDirPaths[0]
2026-01-28 13:13:58 +08:00
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,
})
}
2026-04-10 15:25:21 +08:00
// 处理 nginx 配置文件的列表与读写。
2026-01-28 13:13:58 +08:00
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
}{
2026-04-10 15:25:21 +08:00
{constants.NginxConfigDirPaths[0], "main"},
{constants.NginxConfigDirPaths[1], "sites-available"},
{constants.NginxConfigDirPaths[2], "sites-enabled"},
{constants.NginxConfigDirPaths[3], "conf.d"},
2026-01-28 13:13:58 +08:00
}
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{
2026-04-10 15:25:21 +08:00
constants.NginxConfigDirPaths[0],
constants.NginxConfigDirPaths[1],
constants.NginxConfigDirPaths[2],
constants.NginxConfigDirPaths[3],
constants.NginxConfigDirPaths[4],
2026-01-28 13:13:58 +08:00
}
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{
2026-04-10 15:25:21 +08:00
constants.NginxConfigDirPaths[0],
constants.NginxConfigDirPaths[4],
2026-01-28 13:13:58 +08:00
}
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,
})
}
2026-04-10 15:25:21 +08:00
// ================== Xray 入站管理 ==================
2026-01-28 13:13:58 +08:00
2026-04-10 15:25:21 +08:00
// InboundRequest 表示入站管理请求。
2026-01-28 13:13:58 +08:00
type InboundRequest struct {
Action string `json:"action"`
Inbound map[string]interface{} `json:"inbound,omitempty"`
Tag string `json:"tag,omitempty"`
}
2026-04-10 15:25:21 +08:00
// 处理入站管理请求。
2026-01-28 13:13:58 +08:00
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()
2026-04-10 15:25:21 +08:00
clients, err := xrpc.New(ctx, constants.LocalhostIP, uint16(apiPort))
2026-01-28 13:13:58 +08:00
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 {
2026-01-29 18:01:08 +08:00
// 过滤掉 tag="api" 和空 tagXray 内部入站)
if ib.Tag != "" && ib.Tag != "api" {
tags = append(tags, ib.Tag)
}
2026-01-28 13:13:58 +08:00
}
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)
2026-01-29 18:01:08 +08:00
// 跳过 tag="api" 的入站(Xray 内部 API 入站)
if tag == "api" {
continue
}
2026-01-28 13:13:58 +08:00
ibCopy := make(map[string]interface{})
for k, v := range ib {
ibCopy[k] = v
}
2026-01-29 18:01:08 +08:00
// 如果 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
}
}
2026-01-28 13:13:58 +08:00
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()
2026-04-10 15:25:21 +08:00
clients, err := xrpc.New(ctx, constants.LocalhostIP, uint16(apiPort))
2026-01-28 13:13:58 +08:00
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
}
2026-04-10 15:25:21 +08:00
// 尝试从运行态移除(未运行时报错可忽略)
2026-02-06 19:55:00 +08:00
runtimeErr := h.removeInbound(ctx, clients.Handler, req.Tag)
if runtimeErr != nil {
log.Printf("[Manage] Warning: Failed to remove inbound from runtime: %v", runtimeErr)
}
2026-04-10 15:25:21 +08:00
// 从配置文件移除(主流程)
2026-02-06 19:55:00 +08:00
configErr := h.removeInboundFromConfig(req.Tag)
if configErr != nil {
log.Printf("[Manage] Warning: Failed to remove inbound from config: %v", configErr)
2026-01-28 13:13:58 +08:00
}
2026-04-10 15:25:21 +08:00
// 配置文件操作成功即可视为成功(运行态移除可选)
// 配置改动后若未重启,运行态可能还没有该入站
2026-02-06 20:08:20 +08:00
if configErr != nil {
2026-04-10 15:25:21 +08:00
// 配置文件操作失败
2026-02-06 20:08:20 +08:00
if runtimeErr != nil {
2026-04-10 15:25:21 +08:00
// 两边都失败时,判断是否只是“未找到”错误
2026-02-06 20:08:20 +08:00
if strings.Contains(runtimeErr.Error(), "not enough information") {
2026-04-10 15:25:21 +08:00
// Xray 返回运行态不存在该入站,这属于可接受情况
// 仅返回配置文件错误
2026-02-06 20:08:20 +08:00
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))
}
2026-02-06 19:55:00 +08:00
return
2026-01-28 13:13:58 +08:00
}
2026-04-10 15:25:21 +08:00
// 配置成功时,运行态报错可接受(可能尚未加载)
2026-01-28 13:13:58 +08:00
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'")
}
}
2026-04-10 15:25:21 +08:00
// ================== Xray 出站管理 ==================
2026-01-28 13:13:58 +08:00
2026-04-10 15:25:21 +08:00
// OutboundRequest 表示出站管理请求。
2026-01-28 13:13:58 +08:00
type OutboundRequest struct {
Action string `json:"action"`
Outbound map[string]interface{} `json:"outbound,omitempty"`
Tag string `json:"tag,omitempty"`
}
2026-04-10 15:25:21 +08:00
// 处理出站管理请求。
2026-01-28 13:13:58 +08:00
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()
2026-04-10 15:25:21 +08:00
clients, err := xrpc.New(ctx, constants.LocalhostIP, uint16(apiPort))
2026-01-28 13:13:58 +08:00
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'")
}
}
2026-04-10 15:25:21 +08:00
// ================== Xray 路由管理 ==================
2026-01-28 13:13:58 +08:00
2026-04-10 15:25:21 +08:00
// RoutingRequest 表示路由管理请求。
2026-01-28 13:13:58 +08:00
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"`
}
2026-04-10 15:25:21 +08:00
// 处理路由管理请求。
2026-01-28 13:13:58 +08:00
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.",
})
}
2026-04-10 15:25:21 +08:00
// ================== 辅助函数 ==================
2026-01-28 13:13:58 +08:00
func (h *ManageHandler) findXrayConfigPath() string {
2026-04-10 15:25:21 +08:00
configPaths := constants.DefaultXrayConfigPaths
2026-01-28 13:13:58 +08:00
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)
}
2026-04-10 15:25:21 +08:00
// ================== 扫描 ==================
2026-01-28 13:13:58 +08:00
2026-04-10 15:25:21 +08:00
// ScanResponse 表示扫描接口响应。
2026-01-28 13:13:58 +08:00
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"`
}
2026-04-10 15:25:21 +08:00
// 处理 POST /api/child/scan。
2026-01-28 13:13:58 +08:00
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)
}
2026-04-10 15:25:21 +08:00
// ================== Xray 配置自动补全 ==================
2026-01-28 13:13:58 +08:00
2026-04-10 15:25:21 +08:00
// EnsureXrayConfigResult 表示配置检查结果。
2026-01-28 13:13:58 +08:00
type EnsureXrayConfigResult struct {
ConfigPath string `json:"config_path"`
Modified bool `json:"modified"`
AddedSections []string `json:"added_sections,omitempty"`
Error string `json:"error,omitempty"`
}
2026-04-10 15:25:21 +08:00
// 检查并补全 Xray 配置。
2026-01-28 13:13:58 +08:00
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),
2026-04-10 15:25:21 +08:00
"listen": constants.LocalhostIP,
2026-01-28 13:13:58 +08:00
"protocol": "dokodemo-door",
"settings": map[string]interface{}{
2026-04-10 15:25:21 +08:00
"address": constants.LocalhostIP,
2026-01-28 13:13:58 +08:00
},
}
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...)
}
2026-03-12 17:20:16 +08:00
2026-04-10 15:25:21 +08:00
// ================== 证书部署 ==================
2026-03-12 17:20:16 +08:00
2026-04-10 15:25:21 +08:00
// CertDeployRequest 表示主控端下发的证书部署请求。
2026-03-12 17:20:16 +08:00
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
}
2026-04-10 15:25:21 +08:00
// 处理 POST /api/child/cert/deploy。
2026-03-12 17:20:16 +08:00
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":
2026-04-04 22:13:41 +08:00
return reloadNginx()
2026-03-12 17:20:16 +08:00
case "xray":
return runCommand("systemctl", "restart", "xray")
case "both":
2026-04-04 22:13:41 +08:00
if err := reloadNginx(); err != nil {
2026-03-12 17:20:16 +08:00
return err
}
return runCommand("systemctl", "restart", "xray")
}
return nil
}
2026-04-07 20:58:13 +08:00
func deployNginxSSLConfig(domain string) {
domain = strings.TrimSpace(domain)
if domain == "" {
return
}
2026-04-10 15:25:21 +08:00
confDir := constants.NginxConfigDirPaths[4]
2026-04-07 20:58:13 +08:00
if _, err := os.Stat(confDir); err != nil {
2026-04-10 15:25:21 +08:00
confDir = constants.NginxConfigDirPaths[0]
2026-04-07 20:58:13 +08:00
}
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)
2026-04-10 15:25:21 +08:00
// 写入 conf.d,或通过 include 挂载
2026-04-07 20:58:13 +08:00
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
}
2026-04-10 15:25:21 +08:00
// 确保主 nginx.conf 包含 conf.d/*.conf
2026-04-07 20:58:13 +08:00
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) {
2026-04-10 15:25:21 +08:00
// 在 http 块最后一个右括号前插入 include
2026-04-07 20:58:13 +08:00
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)
}
2026-04-10 15:25:21 +08:00
// HandleNginxSetupSSL 处理 POST /api/child/nginx/setup-ssl。
// 部署 nginx.conf 和 servers/{domain}.conf。
2026-04-08 00:47:08 +08:00
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 {
2026-04-08 10:03:53 +08:00
Domain string `json:"domain"`
NginxConfig string `json:"nginx_config"`
DomainConfig string `json:"domain_config"`
2026-04-08 00:47:08 +08:00
}
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))
2026-04-08 09:45:45 +08:00
2026-04-10 15:25:21 +08:00
confDir := constants.NginxPrimaryPrefixDir
2026-04-08 10:03:53 +08:00
if _, err := os.Stat(confDir); err != nil {
2026-04-10 15:25:21 +08:00
confDir = constants.NginxConfigDirPaths[0]
2026-04-08 10:03:53 +08:00
}
2026-04-08 09:45:45 +08:00
2026-04-10 15:25:21 +08:00
// 确保证书和 servers 目录存在
2026-04-08 10:03:53 +08:00
os.MkdirAll(filepath.Join(confDir, "cert"), 0755)
os.MkdirAll(filepath.Join(confDir, "servers"), 0755)
2026-04-08 09:45:45 +08:00
2026-04-08 10:03:53 +08:00
if req.NginxConfig != "" {
2026-04-10 15:25:21 +08:00
// 下发主 nginx.conf
2026-04-08 09:45:45 +08:00
mainConf := filepath.Join(confDir, "nginx.conf")
if content, err := os.ReadFile(mainConf); err == nil {
2026-04-08 10:03:53 +08:00
os.WriteFile(mainConf+".bak."+time.Now().Format("20060102150405"), content, 0644)
2026-04-08 09:45:45 +08:00
}
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
}
2026-04-08 10:03:53 +08:00
log.Printf("[Manage] nginx.conf deployed at %s", mainConf)
}
if req.DomainConfig != "" {
2026-04-10 15:25:21 +08:00
// 下发域名 server 配置到 servers/{domain}.conf
2026-04-08 10:03:53 +08:00
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)
2026-04-08 09:45:45 +08:00
} else {
2026-04-10 15:25:21 +08:00
// 兜底:沿用旧逻辑
2026-04-08 09:45:45 +08:00
deployNginxSSLConfig(domain)
}
2026-04-08 00:47:08 +08:00
2026-04-10 15:25:21 +08:00
// 重载 nginx 使配置生效
2026-04-08 00:47:08 +08:00
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),
})
}
2026-04-04 22:13:41 +08:00
func reloadNginx() error {
2026-04-10 15:25:21 +08:00
for _, bin := range constants.NginxBinarySearchPaths {
2026-04-04 22:13:41 +08:00
if path, err := exec.LookPath(bin); err == nil {
return runCommand(path, "-s", "reload")
}
}
return runCommand("systemctl", "reload", "nginx")
}
2026-03-12 17:20:16 +08:00
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
}
2026-04-03 15:51:49 +08:00
2026-04-10 15:25:21 +08:00
// 在缺失配置时下发内置默认配置。
2026-04-07 16:35:45 +08:00
func (h *ManageHandler) deployDefaultXrayConfig() {
2026-04-10 15:25:21 +08:00
configPath := constants.DefaultXrayConfigPaths[0]
2026-04-07 16:35:45 +08:00
if _, err := os.Stat(configPath); err == nil {
2026-04-10 15:25:21 +08:00
// 配置已存在,执行 EnsureXrayConfig 补齐缺失段
2026-04-07 16:35:45 +08:00
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()
}
2026-04-10 15:25:21 +08:00
// ================== SSE 流式安装/卸载 ==================
2026-04-03 15:51:49 +08:00
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")
2026-04-07 16:35:45 +08:00
2026-04-10 15:25:21 +08:00
// 安装完成后下发默认配置
2026-04-07 16:35:45 +08:00
h.deployDefaultXrayConfig()
2026-04-03 15:51:49 +08:00
}
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)
2026-04-07 20:58:13 +08:00
domain := r.URL.Query().Get("domain")
2026-04-03 15:51:49 +08:00
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")
2026-04-07 20:58:13 +08:00
if domain != "" {
deployNginxSSLConfig(domain)
}
2026-04-03 15:51:49 +08:00
}
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")
}