1117 lines
28 KiB
Go
1117 lines
28 KiB
Go
/*
|
||
* 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"
|
||
"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/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)
|
||
}
|
||
|
||
// 处理服务器信息
|
||
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)))
|
||
}
|
||
|
||
// extractListenAddr 从插件中提取监听地址
|
||
func extractListenAddr(tag string, plugin any) string {
|
||
// 这里需要根据实际插件类型提取
|
||
// 简化处理:从 tag 中猜测
|
||
if contains(tag, "udp_server") || contains(tag, "tcp_server") {
|
||
return "53"
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// 处理服务器状态
|
||
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,
|
||
})
|
||
}
|
||
|
||
// 处理获取配置
|
||
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("开始重载配置")
|
||
|
||
// 这里需要实现配置重载逻辑
|
||
// 由于当前架构限制,我们先返回成功,实际实现需要重构
|
||
|
||
m.writeJSONResponse(w, APIResponse{
|
||
Success: true,
|
||
Message: "配置重载请求已接收,请重启服务以应用新配置",
|
||
})
|
||
}
|
||
|
||
// 处理验证配置
|
||
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
|
||
}
|
||
|
||
// 设置当前 API 地址
|
||
func SetCurrentAPIAddress(addr string) {
|
||
currentAPIAddress = addr
|
||
}
|