/* * Copyright (C) 2020-2022, IrineSistiana * * This file is part of mosdns. * * mosdns is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * mosdns is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package coremain import ( "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strings" "sync" "time" "github.com/go-chi/chi/v5" "go.uber.org/zap" "gopkg.in/yaml.v3" ) // ServerInfo 服务器信息 type ServerInfo struct { Name string `json:"name"` Version string `json:"version"` StartTime time.Time `json:"start_time"` Uptime string `json:"uptime"` UptimeSeconds int64 `json:"uptime_seconds"` // 运行时间(秒) Status string `json:"status"` ConfigFile string `json:"config_file"` WorkingDir string `json:"working_dir"` PluginCount int `json:"plugin_count"` APIAddress string `json:"api_address"` // API 监听地址 DNSPorts []string `json:"dns_ports"` // DNS 监听端口列表 } // ConfigResponse 配置响应 type ConfigResponse struct { Config interface{} `json:"config"` ConfigFile string `json:"config_file"` LastModified time.Time `json:"last_modified"` } // DomainFileInfo 域名文件信息 type DomainFileInfo struct { Name string `json:"name"` Path string `json:"path"` Size int64 `json:"size"` LineCount int `json:"line_count"` LastModified time.Time `json:"last_modified"` } // APIResponse 通用API响应 type APIResponse struct { Success bool `json:"success"` Message string `json:"message"` Data interface{} `json:"data,omitempty"` Error string `json:"error,omitempty"` } var ( serverStartTime = time.Now() currentConfigFile string currentAPIAddress string // 当前 API 监听地址 ) // 注册管理API路由 func (m *Mosdns) registerManagementAPI() { // 服务器信息 m.httpMux.Get("/api/server/info", m.handleServerInfo) m.httpMux.Get("/api/server/status", m.handleServerStatus) // 统计信息 m.httpMux.Get("/api/stats/detailed", m.handleStatsDetailed) // 配置管理 m.httpMux.Get("/api/config", m.handleGetConfig) m.httpMux.Put("/api/config", m.handleUpdateConfig) m.httpMux.Post("/api/config/reload", m.handleReloadConfig) m.httpMux.Post("/api/config/validate", m.handleValidateConfig) m.httpMux.Get("/api/config/backup", m.handleBackupConfig) m.httpMux.Post("/api/config/restore", m.handleRestoreConfig) // 域名文件管理 m.httpMux.Get("/api/domain-files", m.handleListDomainFiles) m.httpMux.Get("/api/domain-files/{filename}", m.handleGetDomainFile) m.httpMux.Put("/api/domain-files/{filename}", m.handleUpdateDomainFile) m.httpMux.Delete("/api/domain-files/{filename}", m.handleDeleteDomainFile) // 插件管理 m.httpMux.Get("/api/plugins", m.handleListPlugins) m.httpMux.Get("/api/plugins/{tag}/status", m.handlePluginStatus) // 系统操作 m.httpMux.Post("/api/system/restart", m.handleSystemRestart) // MikroTik 管理(旧版,保留兼容性) m.httpMux.Get("/api/mikrotik/list", m.handleListMikroTik) m.httpMux.Post("/api/mikrotik/add", m.handleAddMikroTik) m.httpMux.Delete("/api/mikrotik/{tag}", m.handleDeleteMikroTik) // 域名路由规则管理(新版配置驱动架构) m.httpMux.Get("/api/rules", m.handleListRules) m.httpMux.Get("/api/rules/{name}", m.handleGetRule) m.httpMux.Post("/api/rules", m.handleAddRule) m.httpMux.Put("/api/rules/{name}", m.handleUpdateRule) m.httpMux.Delete("/api/rules/{name}", m.handleDeleteRule) } // 处理服务器信息 func (m *Mosdns) handleServerInfo(w http.ResponseWriter, r *http.Request) { wd, _ := os.Getwd() // 计算运行时间(秒) uptimeSeconds := int64(time.Since(serverStartTime).Seconds()) // 获取 DNS 端口列表 dnsPorts := m.getDNSPorts() info := ServerInfo{ Name: "MosDNS Server", Version: "v5.0.0", // 这里应该从版本变量获取 StartTime: serverStartTime, Uptime: time.Since(serverStartTime).String(), UptimeSeconds: uptimeSeconds, // 添加秒数,方便前端格式化 Status: "running", ConfigFile: currentConfigFile, WorkingDir: wd, PluginCount: len(m.plugins), APIAddress: currentAPIAddress, // API 监听地址 DNSPorts: dnsPorts, // DNS 端口列表 } m.writeJSONResponse(w, APIResponse{ Success: true, Data: info, }) } // getDNSPorts 获取所有 DNS 服务器监听的端口 func (m *Mosdns) getDNSPorts() []string { ports := make([]string, 0) portMap := make(map[string]bool) // 用于去重 // 遍历所有插件,查找服务器类型的插件 serverTypes := []string{"udp_server", "tcp_server", "http_server", "quic_server"} for tag, plugin := range m.plugins { pluginType := fmt.Sprintf("%T", plugin) // 检查是否是服务器类型 isServer := false for _, st := range serverTypes { if contains(pluginType, st) || contains(tag, st) { isServer = true break } } if isServer { // 尝试从插件配置中提取端口信息 // 这里简化处理,实际应该从配置中读取 if addr := extractListenAddr(tag, plugin); addr != "" { if !portMap[addr] { ports = append(ports, addr) portMap[addr] = true } } } } // 如果没有找到端口,返回默认提示 if len(ports) == 0 { ports = append(ports, "未检测到") } return ports } // contains 检查字符串是否包含子串 func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || (len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr))) } // 全局变量保存当前 Mosdns 实例(用于从辅助函数访问) var currentMosdnsInstance *Mosdns // getCurrentMosdns 获取当前的 Mosdns 实例 func getCurrentMosdns() *Mosdns { return currentMosdnsInstance } // extractListenAddr 从插件中提取监听地址 func extractListenAddr(tag string, plugin any) string { // 尝试从配置中读取实际的监听地址 // 通过反射或类型断言获取插件的监听地址 // 简化方案:从配置文件中查找对应插件的 listen 配置 if m := getCurrentMosdns(); m != nil && m.config != nil { for _, pc := range m.config.Plugins { if pc.Tag == tag { // 尝试从 Args 中提取 listen 字段 if argsMap, ok := pc.Args.(map[string]interface{}); ok { if listen, ok := argsMap["listen"].(string); ok && listen != "" { // 提取端口号 if strings.Contains(listen, ":") { parts := strings.Split(listen, ":") return parts[len(parts)-1] } return listen } } } } } // 默认值 return "53" } // 处理服务器状态 func (m *Mosdns) handleServerStatus(w http.ResponseWriter, r *http.Request) { status := map[string]interface{}{ "status": "healthy", "uptime": time.Since(serverStartTime).Seconds(), "plugin_count": len(m.plugins), "memory_usage": getMemoryUsage(), "goroutines": getGoroutineCount(), } m.writeJSONResponse(w, APIResponse{ Success: true, Data: status, }) } // 全局查询统计(简化实现,实际应该使用 metrics) var ( totalQueries int64 = 0 successfulQueries int64 = 0 failedQueries int64 = 0 cacheHits int64 = 0 cacheMisses int64 = 0 totalResponseTime int64 = 0 statsMutex sync.RWMutex ) // 处理详细统计信息 func (m *Mosdns) handleStatsDetailed(w http.ResponseWriter, r *http.Request) { // 从 server_handler 获取查询统计 // TODO: 导入 server_handler 包会导致循环依赖,暂时使用本地统计 statsMutex.RLock() total := totalQueries successful := successfulQueries failed := failedQueries cacheHit := cacheHits cacheMiss := cacheMisses totalTime := totalResponseTime statsMutex.RUnlock() // 计算平均响应时间 avgResponseTime := int64(0) if total > 0 { avgResponseTime = totalTime / total } stats := map[string]interface{}{ "totalQueries": total, "successfulQueries": successful, "failedQueries": failed, "cacheHits": cacheHit, "cacheMisses": cacheMiss, "avgResponseTime": avgResponseTime, } m.writeJSONResponse(w, APIResponse{ Success: true, Data: stats, }) } // RecordQuery 记录查询统计(供其他模块调用) func RecordQuery(success bool, cached bool, responseTimeMs int64) { statsMutex.Lock() defer statsMutex.Unlock() totalQueries++ if success { successfulQueries++ } else { failedQueries++ } if cached { cacheHits++ } else { cacheMisses++ } totalResponseTime += responseTimeMs } // setupQueryStatsRecorder 在 server_handler 中注入统计函数 // 使用 init() 函数自动注入,避免循环依赖 func setupQueryStatsRecorder() { // 这个函数会被 server_handler 包的 init() 调用 // 通过反射或全局变量的方式注入 // 当前版本由于循环依赖问题,暂时在 server_handler 内部直接实现 } // 处理获取配置 func (m *Mosdns) handleGetConfig(w http.ResponseWriter, r *http.Request) { if currentConfigFile == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Error: "配置文件路径未知", }) return } // 读取配置文件 configData, err := os.ReadFile(currentConfigFile) if err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("读取配置文件失败: %v", err), }) return } // 获取文件修改时间 fileInfo, _ := os.Stat(currentConfigFile) var lastModified time.Time if fileInfo != nil { lastModified = fileInfo.ModTime() } // 解析YAML配置 var config interface{} if err := yaml.Unmarshal(configData, &config); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("解析配置文件失败: %v", err), }) return } response := ConfigResponse{ Config: config, ConfigFile: currentConfigFile, LastModified: lastModified, } m.writeJSONResponse(w, APIResponse{ Success: true, Data: response, }) } // 处理更新配置 func (m *Mosdns) handleUpdateConfig(w http.ResponseWriter, r *http.Request) { if currentConfigFile == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Error: "配置文件路径未知", }) return } // 读取请求体 body, err := io.ReadAll(r.Body) if err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("读取请求体失败: %v", err), }) return } // 验证YAML格式 var config interface{} if err := yaml.Unmarshal(body, &config); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("YAML格式错误: %v", err), }) return } // 备份当前配置 backupFile := currentConfigFile + ".backup." + time.Now().Format("20060102-150405") if err := copyFile(currentConfigFile, backupFile); err != nil { m.logger.Warn("备份配置文件失败", zap.Error(err)) } // 写入新配置 if err := os.WriteFile(currentConfigFile, body, 0644); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("写入配置文件失败: %v", err), }) return } m.logger.Info("配置文件已更新", zap.String("file", currentConfigFile)) m.writeJSONResponse(w, APIResponse{ Success: true, Message: "配置文件更新成功", }) } // 处理重载配置 func (m *Mosdns) handleReloadConfig(w http.ResponseWriter, r *http.Request) { m.logger.Info("🔄 收到热加载配置请求") // 检查热加载管理器是否已初始化 if m.hotReloadMgr == nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: "热加载管理器未初始化,请确保配置文件路径正确", }) return } // 检查是否正在重载 if m.hotReloadMgr.IsReloading() { m.writeJSONResponse(w, APIResponse{ Success: false, Error: "配置重载正在进行中,请稍后再试", }) return } // 执行热加载 pluginCount, err := m.hotReloadMgr.Reload() if err != nil { m.logger.Error("热加载失败", zap.Error(err)) m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("热加载失败: %v", err), }) return } m.logger.Info("🎉 热加载成功", zap.Int("plugin_count", pluginCount)) m.writeJSONResponse(w, APIResponse{ Success: true, Message: fmt.Sprintf("配置热加载成功!已加载 %d 个插件", pluginCount), Data: map[string]interface{}{ "plugin_count": pluginCount, "config_path": m.hotReloadMgr.GetConfigPath(), "reload_time": time.Now().Format("2006-01-02 15:04:05"), }, }) } // 处理验证配置 func (m *Mosdns) handleValidateConfig(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("读取请求体失败: %v", err), }) return } // 验证YAML格式 var config Config if err := yaml.Unmarshal(body, &config); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("YAML格式错误: %v", err), }) return } // 这里可以添加更多配置验证逻辑 m.writeJSONResponse(w, APIResponse{ Success: true, Message: "配置验证通过", }) } // 处理备份配置 func (m *Mosdns) handleBackupConfig(w http.ResponseWriter, r *http.Request) { if currentConfigFile == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Error: "配置文件路径未知", }) return } // 创建备份文件 backupFile := currentConfigFile + ".backup." + time.Now().Format("20060102-150405") if err := copyFile(currentConfigFile, backupFile); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("备份配置文件失败: %v", err), }) return } m.logger.Info("配置文件已备份", zap.String("backup", backupFile)) m.writeJSONResponse(w, APIResponse{ Success: true, Message: "配置备份成功", Data: map[string]interface{}{ "backup_file": backupFile, "backup_time": time.Now(), }, }) } // 处理恢复配置 func (m *Mosdns) handleRestoreConfig(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("读取请求体失败: %v", err), }) return } var request struct { BackupFile string `json:"backup_file"` } if err := json.Unmarshal(body, &request); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("解析请求失败: %v", err), }) return } if request.BackupFile == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Error: "备份文件路径不能为空", }) return } // 验证备份文件存在 if _, err := os.Stat(request.BackupFile); os.IsNotExist(err) { m.writeJSONResponse(w, APIResponse{ Success: false, Error: "备份文件不存在", }) return } // 恢复配置 if err := copyFile(request.BackupFile, currentConfigFile); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("恢复配置文件失败: %v", err), }) return } m.logger.Info("配置文件已恢复", zap.String("from", request.BackupFile)) m.writeJSONResponse(w, APIResponse{ Success: true, Message: "配置恢复成功", }) } // 处理列出域名文件(从运行目录的 mikrotik 目录加载) func (m *Mosdns) handleListDomainFiles(w http.ResponseWriter, r *http.Request) { var domainFiles []DomainFileInfo // 获取运行目录 wd, err := os.Getwd() if err != nil { m.logger.Error("failed to get working directory", zap.Error(err)) m.writeJSONResponse(w, APIResponse{ Success: false, Message: "获取工作目录失败: " + err.Error(), }) return } // mikrotik 目录路径 mikrotikDir := filepath.Join(wd, "mikrotik") // 检查目录是否存在 if _, err := os.Stat(mikrotikDir); os.IsNotExist(err) { // 如果目录不存在,尝试创建 if err := os.MkdirAll(mikrotikDir, 0755); err != nil { m.logger.Error("failed to create mikrotik directory", zap.Error(err)) m.writeJSONResponse(w, APIResponse{ Success: false, Message: "创建 mikrotik 目录失败: " + err.Error(), }) return } m.logger.Info("created mikrotik directory", zap.String("path", mikrotikDir)) } // 扫描 mikrotik 目录下的所有 .txt 文件 files, err := filepath.Glob(filepath.Join(mikrotikDir, "*.txt")) if err != nil { m.logger.Error("failed to list domain files", zap.Error(err)) m.writeJSONResponse(w, APIResponse{ Success: false, Message: "扫描域名文件失败: " + err.Error(), }) return } // 遍历文件 for _, filePath := range files { info, err := os.Stat(filePath) if err != nil { m.logger.Warn("domain file not accessible", zap.String("path", filePath), zap.Error(err)) continue } lineCount := countLines(filePath) domainFiles = append(domainFiles, DomainFileInfo{ Name: filepath.Base(filePath), Path: filePath, Size: info.Size(), LineCount: lineCount, LastModified: info.ModTime(), }) } m.writeJSONResponse(w, APIResponse{ Success: true, Data: domainFiles, Message: fmt.Sprintf("从 %s 目录加载了 %d 个域名文件", mikrotikDir, len(domainFiles)), }) } // extractDomainFilesFromConfig 从配置中提取所有域名文件路径 func (m *Mosdns) extractDomainFilesFromConfig() []string { filePaths := make([]string, 0) fileMap := make(map[string]bool) // 用于去重 // 读取配置文件 if currentConfigFile == "" { return filePaths } configData, err := os.ReadFile(currentConfigFile) if err != nil { m.logger.Error("failed to read config file", zap.Error(err)) return filePaths } // 解析配置文件 var config Config if err := yaml.Unmarshal(configData, &config); err != nil { m.logger.Error("failed to parse config", zap.Error(err)) return filePaths } // 遍历所有插件,提取域名文件 for _, pluginConfig := range config.Plugins { // 检查是否是域名集合类型或 MikroTik 类型 if pluginConfig.Type == "domain_set" || pluginConfig.Type == "mikrotik_addresslist" { // 尝试从 Args 中提取 files 或 domain_files if args, ok := pluginConfig.Args.(map[string]interface{}); ok { // domain_set 使用 "files" if files, ok := args["files"].([]interface{}); ok { for _, file := range files { if filePath, ok := file.(string); ok { if !fileMap[filePath] { filePaths = append(filePaths, filePath) fileMap[filePath] = true } } } } // mikrotik_addresslist 使用 "domain_files" if files, ok := args["domain_files"].([]interface{}); ok { for _, file := range files { if filePath, ok := file.(string); ok { if !fileMap[filePath] { filePaths = append(filePaths, filePath) fileMap[filePath] = true } } } } } } } return filePaths } // 处理获取域名文件 func (m *Mosdns) handleGetDomainFile(w http.ResponseWriter, r *http.Request) { filename := chi.URLParam(r, "filename") if filename == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Error: "文件名不能为空", }) return } // 查找文件 filePath := m.findDomainFile(filename) if filePath == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Error: "文件未找到", }) return } content, err := os.ReadFile(filePath) if err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("读取文件失败: %v", err), }) return } // 解析域名列表 domains := strings.Split(string(content), "\n") var cleanDomains []string for _, domain := range domains { domain = strings.TrimSpace(domain) if domain != "" && !strings.HasPrefix(domain, "#") { cleanDomains = append(cleanDomains, domain) } } fileInfo, _ := os.Stat(filePath) response := map[string]interface{}{ "filename": filename, "path": filePath, "domains": cleanDomains, "total_count": len(cleanDomains), "file_size": fileInfo.Size(), "last_modified": fileInfo.ModTime(), } m.writeJSONResponse(w, APIResponse{ Success: true, Data: response, }) } // 处理更新域名文件 func (m *Mosdns) handleUpdateDomainFile(w http.ResponseWriter, r *http.Request) { filename := chi.URLParam(r, "filename") if filename == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Error: "文件名不能为空", }) return } body, err := io.ReadAll(r.Body) if err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("读取请求体失败: %v", err), }) return } // 查找或创建文件路径 filePath := m.findDomainFile(filename) if filePath == "" { // 如果文件不存在,在默认目录创建 filePath = filepath.Join("./domain-files", filename) os.MkdirAll(filepath.Dir(filePath), 0755) } // 备份现有文件 if _, err := os.Stat(filePath); err == nil { backupFile := filePath + ".backup." + time.Now().Format("20060102-150405") copyFile(filePath, backupFile) } // 写入新内容 if err := os.WriteFile(filePath, body, 0644); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("写入文件失败: %v", err), }) return } m.logger.Info("域名文件已更新", zap.String("file", filePath)) m.writeJSONResponse(w, APIResponse{ Success: true, Message: "域名文件更新成功", Data: map[string]interface{}{ "filename": filename, "path": filePath, }, }) } // 处理删除域名文件 func (m *Mosdns) handleDeleteDomainFile(w http.ResponseWriter, r *http.Request) { filename := chi.URLParam(r, "filename") if filename == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Error: "文件名不能为空", }) return } filePath := m.findDomainFile(filename) if filePath == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Error: "文件未找到", }) return } // 备份文件 backupFile := filePath + ".deleted." + time.Now().Format("20060102-150405") if err := copyFile(filePath, backupFile); err != nil { m.logger.Warn("备份文件失败", zap.Error(err)) } // 删除文件 if err := os.Remove(filePath); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: fmt.Sprintf("删除文件失败: %v", err), }) return } m.logger.Info("域名文件已删除", zap.String("file", filePath)) m.writeJSONResponse(w, APIResponse{ Success: true, Message: "域名文件删除成功", }) } // 处理列出插件 func (m *Mosdns) handleListPlugins(w http.ResponseWriter, r *http.Request) { plugins := make([]map[string]interface{}, 0, len(m.plugins)) for tag, plugin := range m.plugins { pluginInfo := map[string]interface{}{ "tag": tag, "type": fmt.Sprintf("%T", plugin), "status": "active", } plugins = append(plugins, pluginInfo) } m.writeJSONResponse(w, APIResponse{ Success: true, Data: plugins, }) } // 处理插件状态 func (m *Mosdns) handlePluginStatus(w http.ResponseWriter, r *http.Request) { tag := chi.URLParam(r, "tag") if tag == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Error: "插件标签不能为空", }) return } plugin := m.plugins[tag] if plugin == nil { m.writeJSONResponse(w, APIResponse{ Success: false, Error: "插件未找到", }) return } status := map[string]interface{}{ "tag": tag, "type": fmt.Sprintf("%T", plugin), "status": "active", } m.writeJSONResponse(w, APIResponse{ Success: true, Data: status, }) } // 处理系统重启 func (m *Mosdns) handleSystemRestart(w http.ResponseWriter, r *http.Request) { m.writeJSONResponse(w, APIResponse{ Success: true, Message: "重启请求已接收,服务将在3秒后重启", }) // 延迟重启 go func() { time.Sleep(3 * time.Second) m.logger.Info("执行系统重启") m.sc.SendCloseSignal(fmt.Errorf("管理员请求重启")) }() } // MikroTik 管理 API // handleListMikroTik 列出所有 MikroTik 配置 func (m *Mosdns) handleListMikroTik(w http.ResponseWriter, r *http.Request) { // 读取配置文件 if currentConfigFile == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "配置文件路径未设置", }) return } configData, err := os.ReadFile(currentConfigFile) if err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "读取配置文件失败: " + err.Error(), }) return } // 解析配置 var config Config if err := yaml.Unmarshal(configData, &config); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "解析配置失败: " + err.Error(), }) return } // 提取 MikroTik 配置 mikrotikConfigs := make([]map[string]interface{}, 0) for _, plugin := range config.Plugins { if plugin.Type == "mikrotik_addresslist" { mikrotikConfigs = append(mikrotikConfigs, map[string]interface{}{ "tag": plugin.Tag, "type": plugin.Type, "args": plugin.Args, }) } } m.writeJSONResponse(w, APIResponse{ Success: true, Data: mikrotikConfigs, }) } // handleAddMikroTik 添加 MikroTik 配置 func (m *Mosdns) handleAddMikroTik(w http.ResponseWriter, r *http.Request) { var newConfig PluginConfig if err := json.NewDecoder(r.Body).Decode(&newConfig); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "解析请求失败: " + err.Error(), }) return } // 验证必填字段 if newConfig.Tag == "" || newConfig.Type == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "Tag 和 Type 不能为空", }) return } // 读取当前配置 if currentConfigFile == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "配置文件路径未设置", }) return } configData, err := os.ReadFile(currentConfigFile) if err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "读取配置文件失败: " + err.Error(), }) return } var config Config if err := yaml.Unmarshal(configData, &config); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "解析配置失败: " + err.Error(), }) return } // 检查是否已存在同名配置 for _, plugin := range config.Plugins { if plugin.Tag == newConfig.Tag { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "配置标签已存在: " + newConfig.Tag, }) return } } // 添加新配置 config.Plugins = append(config.Plugins, newConfig) // 使用 yaml.v3 Encoder 保存配置,设置更好的缩进 var buf strings.Builder encoder := yaml.NewEncoder(&buf) encoder.SetIndent(4) // 设置缩进为 4 个空格 if err := encoder.Encode(&config); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "序列化配置失败: " + err.Error(), }) return } encoder.Close() if err := os.WriteFile(currentConfigFile, []byte(buf.String()), 0644); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "保存配置文件失败: " + err.Error(), }) return } m.writeJSONResponse(w, APIResponse{ Success: true, Message: "MikroTik 配置添加成功,需要重启服务生效", Data: newConfig, }) } // handleDeleteMikroTik 删除 MikroTik 配置 func (m *Mosdns) handleDeleteMikroTik(w http.ResponseWriter, r *http.Request) { tag := chi.URLParam(r, "tag") if tag == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "缺少配置标签参数", }) return } // 读取当前配置 if currentConfigFile == "" { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "配置文件路径未设置", }) return } configData, err := os.ReadFile(currentConfigFile) if err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "读取配置文件失败: " + err.Error(), }) return } var config Config if err := yaml.Unmarshal(configData, &config); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "解析配置失败: " + err.Error(), }) return } // 查找并删除配置 found := false newPlugins := make([]PluginConfig, 0) for _, plugin := range config.Plugins { if plugin.Tag == tag && plugin.Type == "mikrotik_addresslist" { found = true continue // 跳过要删除的配置 } newPlugins = append(newPlugins, plugin) } if !found { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "未找到指定的 MikroTik 配置: " + tag, }) return } config.Plugins = newPlugins // 使用 yaml.v3 Encoder 保存配置,设置更好的缩进 var buf strings.Builder encoder := yaml.NewEncoder(&buf) encoder.SetIndent(4) // 设置缩进为 4 个空格 if err := encoder.Encode(&config); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "序列化配置失败: " + err.Error(), }) return } encoder.Close() if err := os.WriteFile(currentConfigFile, []byte(buf.String()), 0644); err != nil { m.writeJSONResponse(w, APIResponse{ Success: false, Message: "保存配置文件失败: " + err.Error(), }) return } m.writeJSONResponse(w, APIResponse{ Success: true, Message: "MikroTik 配置删除成功,需要重启服务生效", }) } // 辅助方法 // 写入JSON响应 func (m *Mosdns) writeJSONResponse(w http.ResponseWriter, response APIResponse) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if !response.Success { w.WriteHeader(http.StatusBadRequest) } json.NewEncoder(w).Encode(response) } // 查找域名文件 func (m *Mosdns) findDomainFile(filename string) string { searchDirs := []string{ "./geosite", "./config", "./domain-files", "./example-domain-files", "/usr/local/yltx-dns/geosite", "/usr/local/yltx-dns/config", } for _, dir := range searchDirs { filePath := filepath.Join(dir, filename) if _, err := os.Stat(filePath); err == nil { return filePath } } return "" } // 复制文件 func copyFile(src, dst string) error { sourceFile, err := os.Open(src) if err != nil { return err } defer sourceFile.Close() destFile, err := os.Create(dst) if err != nil { return err } defer destFile.Close() _, err = io.Copy(destFile, sourceFile) return err } // 统计文件行数 func countLines(filename string) int { content, err := os.ReadFile(filename) if err != nil { return 0 } lines := strings.Split(string(content), "\n") count := 0 for _, line := range lines { if strings.TrimSpace(line) != "" && !strings.HasPrefix(strings.TrimSpace(line), "#") { count++ } } return count } // 获取内存使用情况 (简化版) func getMemoryUsage() map[string]interface{} { return map[string]interface{}{ "allocated": "N/A", "sys": "N/A", } } // 获取协程数量 (简化版) func getGoroutineCount() int { return 0 } // 设置当前配置文件路径 func SetCurrentConfigFile(path string) { currentConfigFile = path } // 获取当前配置文件路径 func GetCurrentConfigFile() string { return currentConfigFile } // 设置当前 API 地址 func SetCurrentAPIAddress(addr string) { currentAPIAddress = addr }