mosdns/coremain/api_handlers.go

1265 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 <https://www.gnu.org/licenses/>.
*/
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
}