格式化

This commit is contained in:
iluobei
2026-04-10 15:25:21 +08:00
parent e13c81ce7a
commit b3202edb49
17 changed files with 633 additions and 407 deletions
@@ -9,6 +9,8 @@ import (
"strings"
"sync"
"time"
"mmw-agent/internal/constants"
)
type DomainLatencyProbeRequest struct {
@@ -24,7 +26,7 @@ type DomainLatencyProbeResult struct {
Error string `json:"error,omitempty"`
}
// HandleDomainLatencyProbe handles POST /api/child/domains/latency
// 处理 POST /api/child/domains/latency 请求。
func (h *ManageHandler) HandleDomainLatencyProbe(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
@@ -49,13 +51,13 @@ func (h *ManageHandler) HandleDomainLatencyProbe(w http.ResponseWriter, r *http.
timeoutMs := req.TimeoutMs
if timeoutMs <= 0 {
timeoutMs = 2000
timeoutMs = constants.DomainProbeDefaultTimeoutMS
}
if timeoutMs < 200 {
timeoutMs = 200
if timeoutMs < constants.DomainProbeMinTimeoutMS {
timeoutMs = constants.DomainProbeMinTimeoutMS
}
if timeoutMs > 10000 {
timeoutMs = 10000
if timeoutMs > constants.DomainProbeMaxTimeoutMS {
timeoutMs = constants.DomainProbeMaxTimeoutMS
}
timeout := time.Duration(timeoutMs) * time.Millisecond
@@ -64,13 +66,13 @@ func (h *ManageHandler) HandleDomainLatencyProbe(w http.ResponseWriter, r *http.
writeError(w, http.StatusBadRequest, "no valid domain to probe")
return
}
if len(domains) > 200 {
domains = domains[:200]
if len(domains) > constants.DomainProbeMaxCount {
domains = domains[:constants.DomainProbeMaxCount]
}
results := make([]DomainLatencyProbeResult, 0, len(domains))
resultCh := make(chan DomainLatencyProbeResult, len(domains))
sem := make(chan struct{}, 16)
sem := make(chan struct{}, constants.DomainProbeConcurrency)
var wg sync.WaitGroup
for _, domain := range domains {
@@ -206,7 +208,7 @@ func splitHostPortLoose(input string) (host string, port string, ok bool) {
}
}
// Fallback for "domain:443" without brackets handling.
// 兼容不带方括号的 "domain:443" 写法。
idx := strings.LastIndex(s, ":")
if idx <= 0 || idx >= len(s)-1 {
return "", "", false
@@ -17,7 +17,7 @@ import (
"sync/atomic"
"time"
"mmw-agent/internal/config"
"mmw-agent/internal/constants"
"mmw-agent/internal/xrpc"
"github.com/xtls/xray-core/app/proxyman/command"
@@ -26,21 +26,21 @@ import (
var nginxInstalling atomic.Bool
// ManageHandler handles management API requests for child servers
// ManageHandler 处理子端管理接口请求。
type ManageHandler struct {
configToken string
}
// NewManageHandler creates a new management handler
// 创建管理处理器。
func NewManageHandler(configToken string) *ManageHandler {
return &ManageHandler{
configToken: configToken,
}
}
// authenticate checks if the request is authorized (token + User-Agent)
// 校验请求身份(token + User-Agent)。
func (h *ManageHandler) authenticate(r *http.Request) bool {
if r.Header.Get("User-Agent") != config.AgentUserAgent {
if r.Header.Get(constants.HeaderUserAgent) != constants.AgentUserAgent {
return false
}
@@ -48,30 +48,30 @@ func (h *ManageHandler) authenticate(r *http.Request) bool {
return true
}
auth := r.Header.Get("Authorization")
auth := r.Header.Get(constants.HeaderAuthorization)
if auth == "" {
auth = r.Header.Get("MM-Remote-Token")
auth = r.Header.Get(constants.HeaderMMRemoteToken)
}
if auth == "" {
return false
}
if strings.HasPrefix(auth, "Bearer ") {
token := strings.TrimPrefix(auth, "Bearer ")
if strings.HasPrefix(auth, constants.BearerPrefix) {
token := strings.TrimPrefix(auth, constants.BearerPrefix)
return token == h.configToken
}
return auth == h.configToken
}
// writeJSON writes JSON response
// 输出 JSON 响应。
func writeJSON(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
// writeError writes error response
// 输出错误响应。
func writeError(w http.ResponseWriter, statusCode int, message string) {
writeJSON(w, statusCode, map[string]interface{}{
"success": false,
@@ -79,23 +79,23 @@ func writeError(w http.ResponseWriter, statusCode int, message string) {
})
}
// ================== System Services Status ==================
// ================== 系统服务状态 ==================
// ServicesStatusResponse represents the response for services status
// ServicesStatusResponse 表示服务状态查询响应。
type ServicesStatusResponse struct {
Success bool `json:"success"`
Xray *ServiceStatus `json:"xray,omitempty"`
Nginx *ServiceStatus `json:"nginx,omitempty"`
}
// ServiceStatus represents a service status
// ServiceStatus 表示单个服务状态。
type ServiceStatus struct {
Installed bool `json:"installed"`
Running bool `json:"running"`
Version string `json:"version,omitempty"`
}
// HandleServicesStatus handles GET /api/child/services/status
// 处理 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")
@@ -121,8 +121,7 @@ func (h *ManageHandler) getXrayStatus() *ServiceStatus {
xrayPath, err := exec.LookPath("xray")
if err != nil {
commonPaths := []string{"/usr/local/bin/xray", "/usr/bin/xray", "/opt/xray/xray"}
for _, p := range commonPaths {
for _, p := range constants.XrayBinarySearchPaths {
if _, err := os.Stat(p); err == nil {
xrayPath = p
break
@@ -142,7 +141,7 @@ func (h *ManageHandler) getXrayStatus() *ServiceStatus {
}
}
// Check systemctl first
// 优先使用 systemctl 检查
cmd := exec.Command("systemctl", "is-active", "xray")
output, _ := cmd.Output()
if strings.TrimSpace(string(output)) == "active" {
@@ -150,14 +149,14 @@ func (h *ManageHandler) getXrayStatus() *ServiceStatus {
return status
}
// Fallback: check if xray process is running via pgrep
// 兜底:用 pgrep 检查 xray 进程
pgrepCmd := exec.Command("pgrep", "-x", "xray")
if err := pgrepCmd.Run(); err == nil {
status.Running = true
return status
}
// Fallback: check via ps for processes containing "xray"
// 兜底:用 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
@@ -169,12 +168,17 @@ func (h *ManageHandler) getXrayStatus() *ServiceStatus {
func (h *ManageHandler) getNginxStatus() *ServiceStatus {
status := &ServiceStatus{}
// Check PATH first, then compiled install path
// 先查 PATH,再查编译安装路径
nginxPath, err := exec.LookPath("nginx")
if err != nil {
if _, statErr := os.Stat("/usr/local/nginx/sbin/nginx"); statErr == nil {
nginxPath = "/usr/local/nginx/sbin/nginx"
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 {
@@ -190,7 +194,7 @@ func (h *ManageHandler) getNginxStatus() *ServiceStatus {
status.Version = "安装中..."
}
// Check systemctl first
// 优先使用 systemctl 检查
cmd := exec.Command("systemctl", "is-active", "nginx")
output, _ := cmd.Output()
if strings.TrimSpace(string(output)) == "active" {
@@ -198,14 +202,14 @@ func (h *ManageHandler) getNginxStatus() *ServiceStatus {
return status
}
// Fallback: check if nginx process is running via pgrep
// 兜底:用 pgrep 检查 nginx 进程
pgrepCmd := exec.Command("pgrep", "-x", "nginx")
if err := pgrepCmd.Run(); err == nil {
status.Running = true
return status
}
// Fallback: check via ps for nginx master process
// 兜底:用 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
@@ -214,15 +218,15 @@ func (h *ManageHandler) getNginxStatus() *ServiceStatus {
return status
}
// ================== Service Control ==================
// ================== 服务控制 ==================
// ServiceControlRequest represents a service control request
// ServiceControlRequest 表示服务控制请求。
type ServiceControlRequest struct {
Service string `json:"service"`
Action string `json:"action"`
}
// HandleServiceControl handles POST /api/child/services/control
// 处理 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")
@@ -265,9 +269,9 @@ func (h *ManageHandler) HandleServiceControl(w http.ResponseWriter, r *http.Requ
})
}
// ================== Xray Installation ==================
// ================== Xray 安装 ==================
// HandleXrayInstall handles POST /api/child/xray/install
// 处理 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")
@@ -297,7 +301,7 @@ func (h *ManageHandler) HandleXrayInstall(w http.ResponseWriter, r *http.Request
log.Printf("[Manage] Xray installed successfully")
// Deploy default config if no config exists
// 若无配置则下发默认配置
h.deployDefaultXrayConfig()
writeJSON(w, http.StatusOK, map[string]interface{}{
@@ -307,7 +311,7 @@ func (h *ManageHandler) HandleXrayInstall(w http.ResponseWriter, r *http.Request
})
}
// HandleXrayRemove handles POST /api/child/xray/remove
// 处理 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")
@@ -344,9 +348,9 @@ func (h *ManageHandler) HandleXrayRemove(w http.ResponseWriter, r *http.Request)
})
}
// ================== Xray Configuration ==================
// ================== Xray 配置 ==================
// HandleXrayConfig handles GET/POST /api/child/xray/config
// 处理 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")
@@ -364,11 +368,7 @@ func (h *ManageHandler) HandleXrayConfig(w http.ResponseWriter, r *http.Request)
}
func (h *ManageHandler) getXrayConfig(w http.ResponseWriter, r *http.Request) {
configPaths := []string{
"/usr/local/etc/xray/config.json",
"/etc/xray/config.json",
"/opt/xray/config.json",
}
configPaths := constants.DefaultXrayConfigPaths
var configPath string
var content []byte
@@ -413,7 +413,7 @@ func (h *ManageHandler) setXrayConfig(w http.ResponseWriter, r *http.Request) {
configPath := req.Path
if configPath == "" {
configPath = "/usr/local/etc/xray/config.json"
configPath = constants.DefaultXrayConfigPaths[0]
}
dir := filepath.Dir(configPath)
@@ -436,9 +436,9 @@ func (h *ManageHandler) setXrayConfig(w http.ResponseWriter, r *http.Request) {
})
}
// ================== Xray System Configuration ==================
// ================== Xray 系统配置 ==================
// XraySystemConfig represents the system configuration state
// XraySystemConfig 表示 Xray 系统配置状态。
type XraySystemConfig struct {
MetricsEnabled bool `json:"metrics_enabled"`
MetricsListen string `json:"metrics_listen"`
@@ -447,7 +447,7 @@ type XraySystemConfig struct {
GrpcPort int `json:"grpc_port"`
}
// HandleXraySystemConfig handles GET/POST /api/child/xray/system-config
// 处理 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")
@@ -484,7 +484,7 @@ func (h *ManageHandler) getXraySystemConfig(w http.ResponseWriter, r *http.Reque
}
sysConfig := &XraySystemConfig{
MetricsListen: "127.0.0.1:38889",
MetricsListen: constants.DefaultMetricsListen,
GrpcPort: 46736,
}
@@ -605,10 +605,10 @@ func (h *ManageHandler) updateXraySystemConfig(w http.ResponseWriter, r *http.Re
apiInbound := map[string]interface{}{
"tag": "api",
"port": float64(req.GrpcPort),
"listen": "127.0.0.1",
"listen": constants.LocalhostIP,
"protocol": "dokodemo-door",
"settings": map[string]interface{}{
"address": "127.0.0.1",
"address": constants.LocalhostIP,
},
}
config["inbounds"] = append([]interface{}{apiInbound}, inbounds...)
@@ -618,10 +618,10 @@ func (h *ManageHandler) updateXraySystemConfig(w http.ResponseWriter, r *http.Re
map[string]interface{}{
"tag": "api",
"port": float64(req.GrpcPort),
"listen": "127.0.0.1",
"listen": constants.LocalhostIP,
"protocol": "dokodemo-door",
"settings": map[string]interface{}{
"address": "127.0.0.1",
"address": constants.LocalhostIP,
},
},
}
@@ -722,9 +722,9 @@ func (h *ManageHandler) removeAPIRoutingRule(config map[string]interface{}) {
routing["rules"] = newRules
}
// ================== Nginx Installation ==================
// ================== Nginx 安装 ==================
// HandleNginxInstall handles POST /api/child/nginx/install
// 处理 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")
@@ -777,7 +777,7 @@ func (h *ManageHandler) HandleNginxInstall(w http.ResponseWriter, r *http.Reques
})
}
// HandleNginxRemove handles POST /api/child/nginx/remove
// 处理 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")
@@ -814,9 +814,9 @@ func (h *ManageHandler) HandleNginxRemove(w http.ResponseWriter, r *http.Request
})
}
// ================== Nginx Configuration ==================
// ================== Nginx 配置 ==================
// HandleNginxConfig handles GET/POST /api/child/nginx/config
// 处理 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")
@@ -834,10 +834,7 @@ func (h *ManageHandler) HandleNginxConfig(w http.ResponseWriter, r *http.Request
}
func (h *ManageHandler) getNginxConfig(w http.ResponseWriter, r *http.Request) {
configPaths := []string{
"/etc/nginx/nginx.conf",
"/usr/local/nginx/conf/nginx.conf",
}
configPaths := constants.DefaultNginxConfigPaths
var configPath string
var content []byte
@@ -876,7 +873,7 @@ func (h *ManageHandler) setNginxConfig(w http.ResponseWriter, r *http.Request) {
configPath := req.Path
if configPath == "" {
configPath = "/etc/nginx/nginx.conf"
configPath = constants.DefaultNginxConfigPaths[0]
}
backupPath := configPath + ".bak." + time.Now().Format("20060102150405")
@@ -908,9 +905,9 @@ func (h *ManageHandler) setNginxConfig(w http.ResponseWriter, r *http.Request) {
})
}
// ================== System Info ==================
// ================== 系统信息 ==================
// HandleSystemInfo handles GET /api/child/system/info
// 处理 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")
@@ -960,9 +957,9 @@ func (h *ManageHandler) HandleSystemInfo(w http.ResponseWriter, r *http.Request)
writeJSON(w, http.StatusOK, info)
}
// ================== Config Files Management ==================
// ================== 配置文件管理 ==================
// ConfigFileInfo represents a config file entry
// ConfigFileInfo 表示配置文件条目。
type ConfigFileInfo struct {
Name string `json:"name"`
Path string `json:"path"`
@@ -970,7 +967,7 @@ type ConfigFileInfo struct {
ModTime string `json:"mod_time"`
}
// HandleXrayConfigFiles handles listing and managing xray config files
// 处理 xray 配置文件的列表与读写。
func (h *ManageHandler) HandleXrayConfigFiles(w http.ResponseWriter, r *http.Request) {
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
@@ -993,11 +990,7 @@ func (h *ManageHandler) HandleXrayConfigFiles(w http.ResponseWriter, r *http.Req
}
func (h *ManageHandler) listXrayConfigFiles(w http.ResponseWriter, r *http.Request) {
configDirs := []string{
"/usr/local/etc/xray",
"/etc/xray",
"/opt/xray",
}
configDirs := constants.XrayConfigDirPaths
var files []ConfigFileInfo
var baseDir string
@@ -1043,9 +1036,9 @@ func (h *ManageHandler) getXrayConfigFile(w http.ResponseWriter, r *http.Request
file = filepath.Clean(file)
configDirs := []string{
"/usr/local/etc/xray",
"/etc/xray",
"/opt/xray",
constants.XrayConfigDirPaths[0],
constants.XrayConfigDirPaths[1],
constants.XrayConfigDirPaths[2],
}
var filePath string
@@ -1100,9 +1093,9 @@ func (h *ManageHandler) saveXrayConfigFile(w http.ResponseWriter, r *http.Reques
}
configDirs := []string{
"/usr/local/etc/xray",
"/etc/xray",
"/opt/xray",
constants.XrayConfigDirPaths[0],
constants.XrayConfigDirPaths[1],
constants.XrayConfigDirPaths[2],
}
var configDir string
@@ -1114,7 +1107,7 @@ func (h *ManageHandler) saveXrayConfigFile(w http.ResponseWriter, r *http.Reques
}
if configDir == "" {
configDir = "/usr/local/etc/xray"
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
@@ -1145,7 +1138,7 @@ func (h *ManageHandler) saveXrayConfigFile(w http.ResponseWriter, r *http.Reques
})
}
// HandleNginxConfigFiles handles listing and managing nginx config files
// 处理 nginx 配置文件的列表与读写。
func (h *ManageHandler) HandleNginxConfigFiles(w http.ResponseWriter, r *http.Request) {
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
@@ -1172,10 +1165,10 @@ func (h *ManageHandler) listNginxConfigFiles(w http.ResponseWriter, r *http.Requ
dir string
description string
}{
{"/etc/nginx", "main"},
{"/etc/nginx/sites-available", "sites-available"},
{"/etc/nginx/sites-enabled", "sites-enabled"},
{"/etc/nginx/conf.d", "conf.d"},
{constants.NginxConfigDirPaths[0], "main"},
{constants.NginxConfigDirPaths[1], "sites-available"},
{constants.NginxConfigDirPaths[2], "sites-enabled"},
{constants.NginxConfigDirPaths[3], "conf.d"},
}
result := make(map[string][]ConfigFileInfo)
@@ -1218,11 +1211,11 @@ func (h *ManageHandler) getNginxConfigFile(w http.ResponseWriter, r *http.Reques
file = filepath.Clean(file)
allowedDirs := []string{
"/etc/nginx",
"/etc/nginx/sites-available",
"/etc/nginx/sites-enabled",
"/etc/nginx/conf.d",
"/usr/local/nginx/conf",
constants.NginxConfigDirPaths[0],
constants.NginxConfigDirPaths[1],
constants.NginxConfigDirPaths[2],
constants.NginxConfigDirPaths[3],
constants.NginxConfigDirPaths[4],
}
var filePath string
@@ -1283,8 +1276,8 @@ func (h *ManageHandler) saveNginxConfigFile(w http.ResponseWriter, r *http.Reque
req.Path = filepath.Clean(req.Path)
allowedDirs := []string{
"/etc/nginx",
"/usr/local/nginx/conf",
constants.NginxConfigDirPaths[0],
constants.NginxConfigDirPaths[4],
}
allowed := false
@@ -1338,16 +1331,16 @@ func (h *ManageHandler) saveNginxConfigFile(w http.ResponseWriter, r *http.Reque
})
}
// ================== Xray Inbounds Management ==================
// ================== Xray 入站管理 ==================
// InboundRequest represents inbound management request
// InboundRequest 表示入站管理请求。
type InboundRequest struct {
Action string `json:"action"`
Inbound map[string]interface{} `json:"inbound,omitempty"`
Tag string `json:"tag,omitempty"`
}
// HandleInbounds handles inbound management
// 处理入站管理请求。
func (h *ManageHandler) HandleInbounds(w http.ResponseWriter, r *http.Request) {
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
@@ -1412,7 +1405,7 @@ func (h *ManageHandler) getInboundTagsFromGRPC() []string {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
clients, err := xrpc.New(ctx, "127.0.0.1", uint16(apiPort))
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
@@ -1517,7 +1510,7 @@ func (h *ManageHandler) manageInbound(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
clients, err := xrpc.New(ctx, "127.0.0.1", uint16(apiPort))
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
@@ -1557,27 +1550,27 @@ func (h *ManageHandler) manageInbound(w http.ResponseWriter, r *http.Request) {
return
}
// Try to remove from runtime (ignore error if not running)
// 尝试从运行态移除(未运行时报错可忽略)
runtimeErr := h.removeInbound(ctx, clients.Handler, req.Tag)
if runtimeErr != nil {
log.Printf("[Manage] Warning: Failed to remove inbound from runtime: %v", runtimeErr)
}
// Remove from config file (this is the primary operation)
// 从配置文件移除(主流程)
configErr := h.removeInboundFromConfig(req.Tag)
if configErr != nil {
log.Printf("[Manage] Warning: Failed to remove inbound from config: %v", configErr)
}
// Success if config operation succeeded (runtime removal is optional)
// The inbound might not exist in runtime if Xray wasn't restarted after config change
// 配置文件操作成功即可视为成功(运行态移除可选)
// 配置改动后若未重启,运行态可能还没有该入站
if configErr != nil {
// Config file operation failed
// 配置文件操作失败
if runtimeErr != nil {
// Both failed - check if it's just "not found" errors
// 两边都失败时,判断是否只是“未找到”错误
if strings.Contains(runtimeErr.Error(), "not enough information") {
// Xray says the inbound doesn't exist in runtime, which is fine
// Just report config error
// 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))
@@ -1588,7 +1581,7 @@ func (h *ManageHandler) manageInbound(w http.ResponseWriter, r *http.Request) {
return
}
// Config succeeded, runtime error is acceptable (inbound might not be loaded)
// 配置成功时,运行态报错可接受(可能尚未加载)
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Inbound removed successfully",
@@ -1599,16 +1592,16 @@ func (h *ManageHandler) manageInbound(w http.ResponseWriter, r *http.Request) {
}
}
// ================== Xray Outbounds Management ==================
// ================== Xray 出站管理 ==================
// OutboundRequest represents outbound management request
// OutboundRequest 表示出站管理请求。
type OutboundRequest struct {
Action string `json:"action"`
Outbound map[string]interface{} `json:"outbound,omitempty"`
Tag string `json:"tag,omitempty"`
}
// HandleOutbounds handles outbound management
// 处理出站管理请求。
func (h *ManageHandler) HandleOutbounds(w http.ResponseWriter, r *http.Request) {
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
@@ -1673,7 +1666,7 @@ func (h *ManageHandler) manageOutbound(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
clients, err := xrpc.New(ctx, "127.0.0.1", uint16(apiPort))
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
@@ -1726,9 +1719,9 @@ func (h *ManageHandler) manageOutbound(w http.ResponseWriter, r *http.Request) {
}
}
// ================== Xray Routing Management ==================
// ================== Xray 路由管理 ==================
// RoutingRequest represents routing management request
// RoutingRequest 表示路由管理请求。
type RoutingRequest struct {
Action string `json:"action"`
Routing map[string]interface{} `json:"routing,omitempty"`
@@ -1736,7 +1729,7 @@ type RoutingRequest struct {
Index int `json:"index,omitempty"`
}
// HandleRouting handles routing management
// 处理路由管理请求。
func (h *ManageHandler) HandleRouting(w http.ResponseWriter, r *http.Request) {
if !h.authenticate(r) {
writeError(w, http.StatusUnauthorized, "Unauthorized")
@@ -1871,14 +1864,10 @@ func (h *ManageHandler) manageRouting(w http.ResponseWriter, r *http.Request) {
})
}
// ================== Helper Functions ==================
// ================== 辅助函数 ==================
func (h *ManageHandler) findXrayConfigPath() string {
configPaths := []string{
"/usr/local/etc/xray/config.json",
"/etc/xray/config.json",
"/opt/xray/config.json",
}
configPaths := constants.DefaultXrayConfigPaths
for _, p := range configPaths {
if _, err := os.Stat(p); err == nil {
@@ -2124,9 +2113,9 @@ func (h *ManageHandler) removeOutboundFromConfig(tag string) error {
return os.WriteFile(configPath, newContent, 0644)
}
// ================== Scan ==================
// ================== 扫描 ==================
// ScanResponse represents the response for scan operation
// ScanResponse 表示扫描接口响应。
type ScanResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
@@ -2139,7 +2128,7 @@ type ScanResponse struct {
ConfigAddedSections []string `json:"config_added_sections,omitempty"`
}
// HandleScan handles POST /api/child/scan
// 处理 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")
@@ -2220,9 +2209,9 @@ func (h *ManageHandler) HandleScan(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, response)
}
// ================== Xray Config Auto-Complete ==================
// ================== Xray 配置自动补全 ==================
// EnsureXrayConfigResult holds the result of config check
// EnsureXrayConfigResult 表示配置检查结果。
type EnsureXrayConfigResult struct {
ConfigPath string `json:"config_path"`
Modified bool `json:"modified"`
@@ -2230,7 +2219,7 @@ type EnsureXrayConfigResult struct {
Error string `json:"error,omitempty"`
}
// EnsureXrayConfig checks and completes Xray configuration
// 检查并补全 Xray 配置。
func (h *ManageHandler) EnsureXrayConfig() *EnsureXrayConfigResult {
result := &EnsureXrayConfigResult{}
@@ -2279,7 +2268,7 @@ func (h *ManageHandler) EnsureXrayConfig() *EnsureXrayConfigResult {
if _, ok := config["metrics"]; !ok {
config["metrics"] = map[string]interface{}{
"tag": "Metrics",
"listen": "127.0.0.1:38889",
"listen": constants.DefaultMetricsListen,
}
result.AddedSections = append(result.AddedSections, "metrics")
modified = true
@@ -2376,10 +2365,10 @@ func (h *ManageHandler) addAPIInbound(config map[string]interface{}) {
apiInbound := map[string]interface{}{
"tag": "api",
"port": float64(46736),
"listen": "127.0.0.1",
"listen": constants.LocalhostIP,
"protocol": "dokodemo-door",
"settings": map[string]interface{}{
"address": "127.0.0.1",
"address": constants.LocalhostIP,
},
}
@@ -2432,9 +2421,9 @@ func (h *ManageHandler) addAPIRoutingRule(config map[string]interface{}) {
routing["rules"] = append([]interface{}{apiRule}, rules...)
}
// ================== Certificate Deploy ==================
// ================== 证书部署 ==================
// CertDeployRequest represents a certificate deploy request from master
// CertDeployRequest 表示主控端下发的证书部署请求。
type CertDeployRequest struct {
Domain string `json:"domain"`
CertPEM string `json:"cert_pem"`
@@ -2444,7 +2433,7 @@ type CertDeployRequest struct {
Reload string `json:"reload"` // nginx, xray, both, none
}
// HandleCertDeploy handles POST /api/child/cert/deploy
// 处理 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")
@@ -2513,9 +2502,9 @@ func deployNginxSSLConfig(domain string) {
return
}
confDir := "/usr/local/nginx/conf"
confDir := constants.NginxConfigDirPaths[4]
if _, err := os.Stat(confDir); err != nil {
confDir = "/etc/nginx"
confDir = constants.NginxConfigDirPaths[0]
}
certDir := filepath.Join(confDir, "cert")
@@ -2539,7 +2528,7 @@ func deployNginxSSLConfig(domain string) {
}
`, domain, domain, domain)
// Write to conf.d or append via include
// 写入 conf.d,或通过 include 挂载
confDDir := filepath.Join(confDir, "conf.d")
os.MkdirAll(confDDir, 0755)
sslConfPath := filepath.Join(confDDir, "ssl.conf")
@@ -2549,7 +2538,7 @@ func deployNginxSSLConfig(domain string) {
return
}
// Ensure main nginx.conf includes conf.d/*.conf
// 确保主 nginx.conf 包含 conf.d/*.conf
mainConf := filepath.Join(confDir, "nginx.conf")
content, err := os.ReadFile(mainConf)
if err != nil {
@@ -2559,7 +2548,7 @@ func deployNginxSSLConfig(domain string) {
includeDirective := "include conf.d/*.conf;"
if !strings.Contains(string(content), includeDirective) {
// Insert include before the last closing brace of http block
// 在 http 块最后一个右括号前插入 include
text := string(content)
lastBrace := strings.LastIndex(text, "}")
if lastBrace > 0 {
@@ -2574,8 +2563,8 @@ func deployNginxSSLConfig(domain string) {
log.Printf("[Manage] Nginx SSL config deployed for domain %s at %s", domain, sslConfPath)
}
// HandleNginxSetupSSL handles POST /api/child/nginx/setup-ssl
// Deploys nginx.conf + domain server block to servers/{domain}.conf.
// 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")
@@ -2598,17 +2587,17 @@ func (h *ManageHandler) HandleNginxSetupSSL(w http.ResponseWriter, r *http.Reque
domain := strings.ToLower(strings.TrimSpace(req.Domain))
confDir := "/usr/local/nginx"
confDir := constants.NginxPrimaryPrefixDir
if _, err := os.Stat(confDir); err != nil {
confDir = "/etc/nginx"
confDir = constants.NginxConfigDirPaths[0]
}
// Ensure cert and servers directories exist
// 确保证书和 servers 目录存在
os.MkdirAll(filepath.Join(confDir, "cert"), 0755)
os.MkdirAll(filepath.Join(confDir, "servers"), 0755)
if req.NginxConfig != "" {
// Deploy base nginx.conf
// 下发主 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)
@@ -2621,7 +2610,7 @@ func (h *ManageHandler) HandleNginxSetupSSL(w http.ResponseWriter, r *http.Reque
}
if req.DomainConfig != "" {
// Deploy domain-specific server block to servers/{domain}.conf
// 下发域名 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))
@@ -2629,11 +2618,11 @@ func (h *ManageHandler) HandleNginxSetupSSL(w http.ResponseWriter, r *http.Reque
}
log.Printf("[Manage] Domain config deployed at %s", domainConfPath)
} else {
// Fallback: legacy behavior
// 兜底:沿用旧逻辑
deployNginxSSLConfig(domain)
}
// Reload nginx to apply
// 重载 nginx 使配置生效
if err := reloadNginx(); err != nil {
log.Printf("[Manage] Nginx reload after setup-ssl failed: %v", err)
}
@@ -2645,7 +2634,7 @@ func (h *ManageHandler) HandleNginxSetupSSL(w http.ResponseWriter, r *http.Reque
}
func reloadNginx() error {
for _, bin := range []string{"/usr/local/nginx/sbin/nginx", "nginx"} {
for _, bin := range constants.NginxBinarySearchPaths {
if path, err := exec.LookPath(bin); err == nil {
return runCommand(path, "-s", "reload")
}
@@ -2660,11 +2649,11 @@ func runCommand(name string, args ...string) error {
return nil
}
// deployDefaultXrayConfig deploys the embedded default xray config if no config exists.
// 在缺失配置时下发内置默认配置。
func (h *ManageHandler) deployDefaultXrayConfig() {
configPath := "/usr/local/etc/xray/config.json"
configPath := constants.DefaultXrayConfigPaths[0]
if _, err := os.Stat(configPath); err == nil {
// Config already exists — run EnsureXrayConfig to add missing sections
// 配置已存在,执行 EnsureXrayConfig 补齐缺失段
result := h.EnsureXrayConfig()
if result.Modified {
log.Printf("[Manage] Xray config updated after install: added %v", result.AddedSections)
@@ -2685,7 +2674,7 @@ func (h *ManageHandler) deployDefaultXrayConfig() {
exec.Command("systemctl", "restart", "xray").Run()
}
// ================== SSE Streaming Install/Remove ==================
// ================== SSE 流式安装/卸载 ==================
func sseStreamCmd(w http.ResponseWriter, r *http.Request, cmd *exec.Cmd, completeMsg string) {
w.Header().Set("Content-Type", "text/event-stream")
@@ -2775,7 +2764,7 @@ func (h *ManageHandler) HandleXrayInstallStream(w http.ResponseWriter, r *http.R
cmd.Env = os.Environ()
sseStreamCmd(w, r, cmd, "Xray installed successfully")
// Deploy default config after install
// 安装完成后下发默认配置
h.deployDefaultXrayConfig()
}
@@ -7,16 +7,16 @@ import (
"strings"
"mmw-agent/internal/agent"
"mmw-agent/internal/config"
"mmw-agent/internal/constants"
)
// APIHandler handles API requests from the master server (for pull mode)
// APIHandler 处理来自主控端的请求(拉取模式)。
type APIHandler struct {
client *agent.Client
configToken string
}
// NewAPIHandler creates a new API handler
// 创建 API 处理器。
func NewAPIHandler(client *agent.Client, configToken string) *APIHandler {
return &APIHandler{
client: client,
@@ -24,7 +24,7 @@ func NewAPIHandler(client *agent.Client, configToken string) *APIHandler {
}
}
// ServeHTTP handles the HTTP request for traffic data
// 返回流量数据。
func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -32,7 +32,7 @@ func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
if !h.authenticate(r) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
@@ -44,7 +44,7 @@ func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
stats, err := h.client.GetStats()
if err != nil {
log.Printf("[API] Failed to get stats: %v", err)
w.Header().Set("Content-Type", "application/json")
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
@@ -53,14 +53,14 @@ func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"stats": stats,
})
}
// ServeSpeedHTTP handles the HTTP request for speed data
// 返回速率数据。
func (h *APIHandler) ServeSpeedHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -68,7 +68,7 @@ func (h *APIHandler) ServeSpeedHTTP(w http.ResponseWriter, r *http.Request) {
}
if !h.authenticate(r) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
@@ -79,7 +79,7 @@ func (h *APIHandler) ServeSpeedHTTP(w http.ResponseWriter, r *http.Request) {
uploadSpeed, downloadSpeed := h.client.GetSpeed()
w.Header().Set("Content-Type", "application/json")
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"upload_speed": uploadSpeed,
@@ -87,9 +87,9 @@ func (h *APIHandler) ServeSpeedHTTP(w http.ResponseWriter, r *http.Request) {
})
}
// authenticate checks if the request is authorized (token + User-Agent)
// 校验请求身份(token + User-Agent)。
func (h *APIHandler) authenticate(r *http.Request) bool {
if r.Header.Get("User-Agent") != config.AgentUserAgent {
if r.Header.Get(constants.HeaderUserAgent) != constants.AgentUserAgent {
return false
}
@@ -97,13 +97,13 @@ func (h *APIHandler) authenticate(r *http.Request) bool {
return true
}
auth := r.Header.Get("Authorization")
auth := r.Header.Get(constants.HeaderAuthorization)
if auth == "" {
return false
}
if strings.HasPrefix(auth, "Bearer ") {
token := strings.TrimPrefix(auth, "Bearer ")
if strings.HasPrefix(auth, constants.BearerPrefix) {
token := strings.TrimPrefix(auth, constants.BearerPrefix)
return token == h.configToken
}
+41
View File
@@ -0,0 +1,41 @@
package handler
import (
"net/http"
"mmw-agent/internal/constants"
)
// 注册子端 API 路由
func RegisterChildRoutes(mux *http.ServeMux, apiHandler *APIHandler, manageHandler *ManageHandler) {
// 拉取模式数据接口
mux.HandleFunc(constants.PathChildTraffic, apiHandler.ServeHTTP)
mux.HandleFunc(constants.PathChildSpeed, apiHandler.ServeSpeedHTTP)
// 管理接口
mux.HandleFunc(constants.PathChildServiceStats, manageHandler.HandleServicesStatus)
mux.HandleFunc(constants.PathChildServiceCtl, manageHandler.HandleServiceControl)
mux.HandleFunc(constants.PathChildXrayInstall, manageHandler.HandleXrayInstall)
mux.HandleFunc(constants.PathChildXrayRemove, manageHandler.HandleXrayRemove)
mux.HandleFunc(constants.PathChildXrayConfig, manageHandler.HandleXrayConfig)
mux.HandleFunc(constants.PathChildXraySysCfg, manageHandler.HandleXraySystemConfig)
mux.HandleFunc(constants.PathChildXrayCfgFiles, manageHandler.HandleXrayConfigFiles)
mux.HandleFunc(constants.PathChildNginxInstall, manageHandler.HandleNginxInstall)
mux.HandleFunc(constants.PathChildNginxRemove, manageHandler.HandleNginxRemove)
mux.HandleFunc(constants.PathChildNginxConfig, manageHandler.HandleNginxConfig)
mux.HandleFunc(constants.PathChildNginxCfgFile, manageHandler.HandleNginxConfigFiles)
mux.HandleFunc(constants.PathChildSystemInfo, manageHandler.HandleSystemInfo)
mux.HandleFunc(constants.PathChildInbounds, manageHandler.HandleInbounds)
mux.HandleFunc(constants.PathChildOutbounds, manageHandler.HandleOutbounds)
mux.HandleFunc(constants.PathChildRouting, manageHandler.HandleRouting)
mux.HandleFunc(constants.PathChildScan, manageHandler.HandleScan)
mux.HandleFunc(constants.PathChildCertDeploy, manageHandler.HandleCertDeploy)
mux.HandleFunc(constants.PathChildNginxSetup, manageHandler.HandleNginxSetupSSL)
mux.HandleFunc(constants.PathChildDomainProbe, manageHandler.HandleDomainLatencyProbe)
// SSE 流式安装和卸载接口
mux.HandleFunc(constants.PathChildXrayInstallStream, manageHandler.HandleXrayInstallStream)
mux.HandleFunc(constants.PathChildXrayRemoveStream, manageHandler.HandleXrayRemoveStream)
mux.HandleFunc(constants.PathChildNginxInstallSSE, manageHandler.HandleNginxInstallStream)
mux.HandleFunc(constants.PathChildNginxRemoveSSE, manageHandler.HandleNginxRemoveStream)
}