first commit
Some checks failed
CI/CD Pipeline / Test (push) Failing after 22m19s
CI/CD Pipeline / Security Scan (push) Failing after 5m57s
CI/CD Pipeline / Build (amd64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (amd64, linux) (push) Has been skipped
CI/CD Pipeline / Build (amd64, windows) (push) Has been skipped
CI/CD Pipeline / Build (arm64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (arm64, linux) (push) Has been skipped
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
Some checks failed
CI/CD Pipeline / Test (push) Failing after 22m19s
CI/CD Pipeline / Security Scan (push) Failing after 5m57s
CI/CD Pipeline / Build (amd64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (amd64, linux) (push) Has been skipped
CI/CD Pipeline / Build (amd64, windows) (push) Has been skipped
CI/CD Pipeline / Build (arm64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (arm64, linux) (push) Has been skipped
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
This commit is contained in:
939
internal/handler/conversion.go
Normal file
939
internal/handler/conversion.go
Normal file
@@ -0,0 +1,939 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
netURL "net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/subconverter-go/internal/cache"
|
||||
"github.com/subconverter-go/internal/config"
|
||||
"github.com/subconverter-go/internal/conversion"
|
||||
"github.com/subconverter-go/internal/logging"
|
||||
)
|
||||
|
||||
// ConversionHandler 代理订阅转换处理器
|
||||
// 处理各种代理订阅格式的转换请求
|
||||
type ConversionHandler struct {
|
||||
logger *logging.Logger
|
||||
configMgr *config.ConfigManager
|
||||
engine *conversion.ConversionEngine
|
||||
cacheMgr *cache.CacheManager
|
||||
}
|
||||
|
||||
// NewConversionHandler 创建新的转换处理器
|
||||
// 返回初始化好的ConversionHandler实例
|
||||
func NewConversionHandler(engine *conversion.ConversionEngine, cacheMgr *cache.CacheManager, configMgr *config.ConfigManager, logger *logging.Logger) *ConversionHandler {
|
||||
return &ConversionHandler{
|
||||
logger: logger,
|
||||
configMgr: configMgr,
|
||||
engine: engine,
|
||||
cacheMgr: cacheMgr,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleConversion 处理转换请求
|
||||
// 支持GET和POST方法,接收订阅URL和转换参数
|
||||
func (h *ConversionHandler) HandleConversion(c *fiber.Ctx) error {
|
||||
h.logger.Info("Conversion request received")
|
||||
|
||||
// 解析请求参数
|
||||
req, err := h.parseConversionRequest(c)
|
||||
if err != nil {
|
||||
h.logger.WithError(err).Error("Failed to parse conversion request")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
if h.cacheMgr.IsEnabled() {
|
||||
cacheKey := h.cacheMgr.GenerateKey(req)
|
||||
if cachedResponse, exists := h.cacheMgr.Get(cacheKey); exists {
|
||||
h.logger.Debugf("Cache hit for conversion request")
|
||||
return h.handleResponse(c, cachedResponse, req)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行转换
|
||||
resp, err := h.engine.Convert(req)
|
||||
if err != nil {
|
||||
h.logger.WithError(err).Error("Conversion failed")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// 缓存响应
|
||||
if h.cacheMgr.IsEnabled() && resp.Success {
|
||||
cacheKey := h.cacheMgr.GenerateKey(req)
|
||||
if err := h.cacheMgr.Set(cacheKey, resp); err != nil {
|
||||
h.logger.WithError(err).Warn("Failed to cache conversion result")
|
||||
}
|
||||
}
|
||||
|
||||
return h.handleResponse(c, resp, req)
|
||||
}
|
||||
|
||||
// handleResponse 处理响应输出
|
||||
func (h *ConversionHandler) handleResponse(c *fiber.Ctx, resp *conversion.ConversionResponse, req *conversion.ConversionRequest) error {
|
||||
if !resp.Success {
|
||||
status := fiber.StatusInternalServerError
|
||||
if resp.ErrorCode == "VALIDATION_ERROR" {
|
||||
status = fiber.StatusBadRequest
|
||||
}
|
||||
return c.Status(status).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": resp.Error,
|
||||
"message": resp.ErrorDetail,
|
||||
})
|
||||
}
|
||||
|
||||
// 根据请求类型返回响应
|
||||
if req.Filename != "" {
|
||||
c.Set("Content-Type", resp.ContentType)
|
||||
c.Set("Content-Disposition", `attachment; filename="`+req.Filename+`"`)
|
||||
return c.Status(http.StatusOK).SendString(resp.Content)
|
||||
}
|
||||
|
||||
// 如果是API请求,返回JSON
|
||||
if strings.HasPrefix(c.Path(), "/api/") {
|
||||
return c.Status(http.StatusOK).JSON(fiber.Map{
|
||||
"success": resp.Success,
|
||||
"content": resp.Content,
|
||||
"target_format": resp.TargetFormat,
|
||||
"source_url": resp.SourceURL,
|
||||
"node_count": resp.NodeCount,
|
||||
"processing_time": resp.ProcessingTime,
|
||||
"timestamp": resp.Timestamp,
|
||||
"headers": resp.Headers,
|
||||
"debug_info": resp.DebugInfo,
|
||||
"request_options": buildRequestOptions(req),
|
||||
})
|
||||
}
|
||||
|
||||
// 默认返回纯文本
|
||||
c.Set("Content-Type", resp.ContentType)
|
||||
return c.Status(http.StatusOK).SendString(resp.Content)
|
||||
}
|
||||
|
||||
// parseConversionRequest 解析转换请求
|
||||
// 从GET查询参数或POST body中解析转换参数
|
||||
func (h *ConversionHandler) parseConversionRequest(c *fiber.Ctx) (*conversion.ConversionRequest, error) {
|
||||
req := conversion.NewConversionRequest()
|
||||
req.UserAgent = c.Get("User-Agent")
|
||||
config := h.configMgr.GetConfig()
|
||||
req.BasePath = h.configMgr.GetBasePath()
|
||||
req.Target = strings.ToLower(config.Conversion.DefaultTarget)
|
||||
req.Emoji = config.Conversion.DefaultEmoji
|
||||
req.UDP = config.Conversion.DefaultUDP
|
||||
|
||||
queryBytes := c.Request().URI().QueryString()
|
||||
if err := req.FromQuery(string(queryBytes)); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
req.Target = strings.ToLower(req.Target)
|
||||
|
||||
if req.Target == "auto" {
|
||||
resolved := resolveAutoTarget(c.Get("User-Agent"))
|
||||
if resolved == "" {
|
||||
resolved = strings.ToLower(config.Conversion.DefaultTarget)
|
||||
if resolved == "" {
|
||||
resolved = "clash"
|
||||
}
|
||||
}
|
||||
req.Target = resolved
|
||||
}
|
||||
|
||||
if len(req.GetSources()) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "subscription URL is required")
|
||||
}
|
||||
|
||||
for _, src := range req.GetSources() {
|
||||
if _, err := netURL.Parse(src); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid subscription URL")
|
||||
}
|
||||
}
|
||||
|
||||
if c.Query("emoji") == "" {
|
||||
req.Emoji = config.Conversion.DefaultEmoji
|
||||
}
|
||||
if c.Query("udp") == "" {
|
||||
req.UDP = config.Conversion.DefaultUDP
|
||||
}
|
||||
|
||||
// 如果是POST请求,尝试从body获取参数
|
||||
if c.Method() == "POST" {
|
||||
var body struct {
|
||||
Target string `json:"target"`
|
||||
URL string `json:"url"`
|
||||
Emoji *bool `json:"emoji"`
|
||||
UDP *bool `json:"udp"`
|
||||
IPv6 *bool `json:"ipv6"`
|
||||
Include []string `json:"include"`
|
||||
Exclude []string `json:"exclude"`
|
||||
Location string `json:"location"`
|
||||
ConfigURL string `json:"config"`
|
||||
Group string `json:"group"`
|
||||
Insert *bool `json:"insert"`
|
||||
Strict *bool `json:"strict"`
|
||||
Compatible *bool `json:"compatible"`
|
||||
Test *bool `json:"test"`
|
||||
List *bool `json:"list"`
|
||||
Filename string `json:"filename"`
|
||||
}
|
||||
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid request body")
|
||||
}
|
||||
|
||||
// 用body中的值覆盖查询参数
|
||||
if body.Target != "" {
|
||||
req.Target = body.Target
|
||||
}
|
||||
if body.URL != "" {
|
||||
if err := req.SetURL(body.URL); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
}
|
||||
if body.Emoji != nil {
|
||||
req.Emoji = *body.Emoji
|
||||
}
|
||||
if body.UDP != nil {
|
||||
req.UDP = *body.UDP
|
||||
}
|
||||
if body.IPv6 != nil {
|
||||
req.IPv6 = *body.IPv6
|
||||
}
|
||||
if len(body.Include) > 0 {
|
||||
req.Include = body.Include
|
||||
}
|
||||
if len(body.Exclude) > 0 {
|
||||
req.Exclude = body.Exclude
|
||||
}
|
||||
if body.Location != "" {
|
||||
req.Location = body.Location
|
||||
}
|
||||
if body.ConfigURL != "" {
|
||||
req.ConfigURL = body.ConfigURL
|
||||
}
|
||||
if body.Group != "" {
|
||||
req.Group = body.Group
|
||||
}
|
||||
if body.Insert != nil {
|
||||
req.Insert = *body.Insert
|
||||
}
|
||||
if body.Strict != nil {
|
||||
req.Strict = *body.Strict
|
||||
}
|
||||
if body.Compatible != nil {
|
||||
req.Compatible = *body.Compatible
|
||||
}
|
||||
if body.Test != nil {
|
||||
req.Test = *body.Test
|
||||
}
|
||||
if body.List != nil {
|
||||
req.List = *body.List
|
||||
}
|
||||
if body.Filename != "" {
|
||||
req.Filename = body.Filename
|
||||
}
|
||||
}
|
||||
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// HandleConvertLegacy 处理传统格式的转换请求
|
||||
// 为了与原C++版本兼容
|
||||
func (h *ConversionHandler) HandleConvertLegacy(c *fiber.Ctx) error {
|
||||
h.logger.Info("Legacy conversion request received")
|
||||
|
||||
// 转发到新的处理器
|
||||
return h.HandleConversion(c)
|
||||
}
|
||||
|
||||
// HandleSub 处理订阅转换请求
|
||||
// 简化的订阅转换接口
|
||||
func (h *ConversionHandler) HandleSub(c *fiber.Ctx) error {
|
||||
h.logger.Info("Subscription conversion request received")
|
||||
|
||||
// 解析查询参数
|
||||
target := c.Query("target")
|
||||
if target == "" {
|
||||
target = "clash" // 默认目标格式
|
||||
}
|
||||
|
||||
url := c.Query("url")
|
||||
if url == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "subscription URL is required",
|
||||
})
|
||||
}
|
||||
|
||||
// 创建简单的转换请求
|
||||
req := conversion.NewConversionRequest()
|
||||
req.Target = target
|
||||
if err := req.SetURL(url); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
req.Emoji = false
|
||||
req.UDP = false
|
||||
|
||||
if err := req.Validate(); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// 执行转换
|
||||
resp, err := h.engine.Convert(req)
|
||||
if err != nil {
|
||||
h.logger.WithError(err).Error("Subscription conversion failed")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// 直接返回配置内容
|
||||
c.Set("Content-Type", "text/plain")
|
||||
return c.SendString(resp.Content)
|
||||
}
|
||||
|
||||
// GetSupportedTargets 获取支持的目标格式列表
|
||||
func (h *ConversionHandler) GetSupportedTargets(c *fiber.Ctx) error {
|
||||
h.logger.Info("Get supported targets request received")
|
||||
|
||||
config := h.configMgr.GetConfig()
|
||||
targets := config.Conversion.SupportedTargets
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"supported_targets": targets,
|
||||
"default_target": config.Conversion.DefaultTarget,
|
||||
})
|
||||
}
|
||||
|
||||
// ValidateURL 验证订阅URL的有效性
|
||||
func (h *ConversionHandler) ValidateURL(c *fiber.Ctx) error {
|
||||
h.logger.Info("Validate URL request received")
|
||||
|
||||
urlStr := c.Query("url")
|
||||
if urlStr == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "URL is required",
|
||||
})
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
parsedURL, err := netURL.Parse(urlStr)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "invalid URL format",
|
||||
})
|
||||
}
|
||||
|
||||
// 检查协议是否支持
|
||||
supportedSchemes := map[string]bool{
|
||||
"http": true,
|
||||
"https": true,
|
||||
"ss": true,
|
||||
"ssr": true,
|
||||
"vmess": true,
|
||||
"trojan": true,
|
||||
}
|
||||
|
||||
if !supportedSchemes[parsedURL.Scheme] {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "unsupported URL scheme",
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"valid": true,
|
||||
"url": urlStr,
|
||||
"scheme": parsedURL.Scheme,
|
||||
"host": parsedURL.Host,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleCacheStats 获取缓存统计信息
|
||||
func (h *ConversionHandler) HandleCacheStats(c *fiber.Ctx) error {
|
||||
h.logger.Info("Cache stats request received")
|
||||
|
||||
if !h.cacheMgr.IsEnabled() {
|
||||
return c.JSON(fiber.Map{
|
||||
"enabled": false,
|
||||
"message": "Cache is disabled",
|
||||
})
|
||||
}
|
||||
|
||||
stats := h.cacheMgr.GetStats()
|
||||
return c.JSON(fiber.Map{
|
||||
"enabled": true,
|
||||
"hits": stats.Hits,
|
||||
"misses": stats.Misses,
|
||||
"evictions": stats.Evictions,
|
||||
"size": stats.Size,
|
||||
"total_size": stats.TotalSize,
|
||||
"hit_rate": stats.HitRate,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleCacheClear 清除缓存
|
||||
func (h *ConversionHandler) HandleCacheClear(c *fiber.Ctx) error {
|
||||
h.logger.Info("Cache clear request received")
|
||||
|
||||
if !h.cacheMgr.IsEnabled() {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Cache is disabled",
|
||||
})
|
||||
}
|
||||
|
||||
h.cacheMgr.Clear()
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"message": "Cache cleared successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// HandleGetConfig 获取配置信息
|
||||
func (h *ConversionHandler) HandleGetConfig(c *fiber.Ctx) error {
|
||||
h.logger.Info("Get config request received")
|
||||
|
||||
config := h.configMgr.GetConfig()
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"config": config,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleUpdateConfig 更新配置
|
||||
func (h *ConversionHandler) HandleUpdateConfig(c *fiber.Ctx) error {
|
||||
h.logger.Info("Update config request received")
|
||||
|
||||
mode := strings.ToLower(c.Query("type"))
|
||||
if mode != "form" && mode != "direct" {
|
||||
c.Set("Content-Type", "text/plain")
|
||||
return c.Status(fiber.StatusNotImplemented).SendString("Not Implemented\n")
|
||||
}
|
||||
|
||||
content := c.Body()
|
||||
if len(content) == 0 {
|
||||
h.logger.Warn("Empty configuration payload received")
|
||||
c.Set("Content-Type", "text/plain")
|
||||
return c.Status(fiber.StatusBadRequest).SendString("Invalid request\n")
|
||||
}
|
||||
|
||||
configPath := h.configMgr.GetFilePath()
|
||||
if configPath == "" {
|
||||
h.logger.Error("Configuration path is not set; cannot update config")
|
||||
c.Set("Content-Type", "text/plain")
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("Config path not configured\n")
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, content, 0o644); err != nil {
|
||||
h.logger.WithError(err).Error("Failed to write configuration file")
|
||||
c.Set("Content-Type", "text/plain")
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("Failed to update configuration\n")
|
||||
}
|
||||
|
||||
if err := h.configMgr.ReloadConfig(); err != nil {
|
||||
h.logger.WithError(err).Error("Failed to reload configuration after update")
|
||||
c.Set("Content-Type", "text/plain")
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("Failed to reload configuration\n")
|
||||
}
|
||||
|
||||
// 清除缓存以避免使用旧配置
|
||||
if h.cacheMgr != nil {
|
||||
h.cacheMgr.Clear()
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/plain")
|
||||
return c.Status(http.StatusOK).SendString("done\n")
|
||||
}
|
||||
|
||||
// HandleReadConf 重新加载配置文件
|
||||
func (h *ConversionHandler) HandleReadConf(c *fiber.Ctx) error {
|
||||
h.logger.Info("Read config request received")
|
||||
|
||||
if err := h.configMgr.ReloadConfig(); err != nil {
|
||||
h.logger.WithError(err).Error("Failed to reload configuration")
|
||||
c.Set("Content-Type", "text/plain")
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("Failed to reload configuration\n")
|
||||
}
|
||||
|
||||
if h.cacheMgr != nil {
|
||||
// 配置可能改变缓存策略,清空缓存以保持一致
|
||||
h.cacheMgr.Clear()
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/plain")
|
||||
return c.Status(http.StatusOK).SendString("done\n")
|
||||
}
|
||||
|
||||
// HandleRefreshRules 重新加载模板或规则资源
|
||||
func (h *ConversionHandler) HandleRefreshRules(c *fiber.Ctx) error {
|
||||
h.logger.Info("Refresh rules request received")
|
||||
|
||||
if h.configMgr.HasTemplateManager() {
|
||||
if err := h.configMgr.ReloadTemplates(); err != nil {
|
||||
h.logger.WithError(err).Warn("Failed to reload templates during refresh rules")
|
||||
}
|
||||
} else {
|
||||
h.logger.Warn("Template manager not available, skipping refresh")
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/plain")
|
||||
return c.Status(http.StatusOK).SendString("done\n")
|
||||
}
|
||||
|
||||
// HandleFlushCache 清空订阅缓存
|
||||
func (h *ConversionHandler) HandleFlushCache(c *fiber.Ctx) error {
|
||||
h.logger.Info("Flush cache request received")
|
||||
|
||||
if h.cacheMgr != nil {
|
||||
h.cacheMgr.Clear()
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/plain")
|
||||
return c.Status(http.StatusOK).SendString("done")
|
||||
}
|
||||
|
||||
// HandleSubscription 处理订阅转换请求(简化版)
|
||||
func (h *ConversionHandler) HandleSubscription(c *fiber.Ctx) error {
|
||||
h.logger.Info("Subscription request received")
|
||||
|
||||
req, err := h.parseConversionRequest(c)
|
||||
if err != nil {
|
||||
var fiberErr *fiber.Error
|
||||
if errors.As(err, &fiberErr) {
|
||||
h.logger.WithError(err).Warn("Invalid subscription request")
|
||||
return sendPlainText(c, fiberErr.Code, fiberErr.Message)
|
||||
}
|
||||
h.logger.WithError(err).Warn("Invalid subscription request")
|
||||
return sendPlainText(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
resp, convErr := h.engine.Convert(req)
|
||||
if convErr != nil {
|
||||
h.logger.WithError(convErr).Error("Subscription conversion failed")
|
||||
return sendPlainText(c, fiber.StatusInternalServerError, convErr.Error())
|
||||
}
|
||||
if !resp.Success {
|
||||
h.logger.WithField("error", resp.Error).Warn("Subscription conversion unsuccessful")
|
||||
return sendPlainText(c, fiber.StatusInternalServerError, resp.Error)
|
||||
}
|
||||
|
||||
contentType := resp.ContentType
|
||||
if contentType == "" {
|
||||
contentType = "text/plain; charset=utf-8"
|
||||
}
|
||||
c.Set("Content-Type", contentType)
|
||||
|
||||
if req.Filename != "" {
|
||||
disposition := fmt.Sprintf("attachment; filename=\"%s\"; filename*=utf-8''%s", req.Filename, netURL.QueryEscape(req.Filename))
|
||||
c.Set("Content-Disposition", disposition)
|
||||
}
|
||||
|
||||
if c.Method() == fiber.MethodHead {
|
||||
c.Status(http.StatusOK)
|
||||
c.Response().SetBodyRaw(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.Status(http.StatusOK).SendString(resp.Content)
|
||||
}
|
||||
|
||||
// HandleSubToClashR 支持 /sub2clashr 兼容端点
|
||||
func (h *ConversionHandler) HandleSubToClashR(c *fiber.Ctx) error {
|
||||
h.logger.Info("Legacy sub2clashr request received")
|
||||
|
||||
sublink := c.Query("sublink")
|
||||
if sublink == "" {
|
||||
return sendPlainText(c, http.StatusBadRequest, "Invalid request!")
|
||||
}
|
||||
if strings.EqualFold(sublink, "sublink") {
|
||||
return sendPlainText(c, http.StatusBadRequest, "Please insert your subscription link instead of clicking the default link.")
|
||||
}
|
||||
|
||||
req := conversion.NewConversionRequest()
|
||||
req.Target = "clashr"
|
||||
if err := req.SetURL(sublink); err != nil {
|
||||
return sendPlainText(c, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
config := h.configMgr.GetConfig()
|
||||
req.Emoji = config.Conversion.DefaultEmoji
|
||||
req.UDP = config.Conversion.DefaultUDP
|
||||
|
||||
if err := req.Validate(); err != nil {
|
||||
return sendPlainText(c, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
resp, err := h.engine.Convert(req)
|
||||
if err != nil {
|
||||
h.logger.WithError(err).Error("Sub2ClashR conversion failed")
|
||||
return sendPlainText(c, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if !resp.Success {
|
||||
h.logger.WithField("error", resp.Error).Error("Sub2ClashR conversion unsuccessful")
|
||||
return sendPlainText(c, http.StatusInternalServerError, resp.Error)
|
||||
}
|
||||
|
||||
contentType := resp.ContentType
|
||||
if contentType == "" {
|
||||
contentType = "text/plain; charset=utf-8"
|
||||
}
|
||||
c.Set("Content-Type", contentType)
|
||||
return c.Status(http.StatusOK).SendString(resp.Content)
|
||||
}
|
||||
|
||||
// HandleSurgeToClash 支持 /surge2clash 兼容端点
|
||||
func (h *ConversionHandler) HandleSurgeToClash(c *fiber.Ctx) error {
|
||||
h.logger.Info("Legacy surge2clash request received")
|
||||
|
||||
link := c.Query("link")
|
||||
if link == "" {
|
||||
return sendPlainText(c, http.StatusBadRequest, "Invalid request!")
|
||||
}
|
||||
if strings.EqualFold(link, "link") {
|
||||
return sendPlainText(c, http.StatusBadRequest, "Please insert your subscription link instead of clicking the default link.")
|
||||
}
|
||||
|
||||
req := conversion.NewConversionRequest()
|
||||
req.Target = "clash"
|
||||
if err := req.SetURL(link); err != nil {
|
||||
return sendPlainText(c, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
config := h.configMgr.GetConfig()
|
||||
req.Emoji = config.Conversion.DefaultEmoji
|
||||
req.UDP = config.Conversion.DefaultUDP
|
||||
|
||||
if err := req.Validate(); err != nil {
|
||||
return sendPlainText(c, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
resp, err := h.engine.Convert(req)
|
||||
if err != nil {
|
||||
h.logger.WithError(err).Error("Surge2Clash conversion failed")
|
||||
return sendPlainText(c, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if !resp.Success {
|
||||
h.logger.WithField("error", resp.Error).Error("Surge2Clash conversion unsuccessful")
|
||||
return sendPlainText(c, http.StatusInternalServerError, resp.Error)
|
||||
}
|
||||
|
||||
contentType := resp.ContentType
|
||||
if contentType == "" {
|
||||
contentType = "text/plain; charset=utf-8"
|
||||
}
|
||||
c.Set("Content-Type", contentType)
|
||||
return c.Status(http.StatusOK).SendString(resp.Content)
|
||||
}
|
||||
|
||||
// HandleGetRemote 获取远程内容,兼容 /get 端点
|
||||
func (h *ConversionHandler) HandleGetRemote(c *fiber.Ctx) error {
|
||||
h.logger.Info("Legacy get request received")
|
||||
|
||||
rawURL := c.Query("url")
|
||||
if rawURL == "" {
|
||||
return sendPlainText(c, http.StatusBadRequest, "Invalid request!")
|
||||
}
|
||||
|
||||
parsed, err := netURL.Parse(rawURL)
|
||||
if err != nil || parsed.Scheme == "" {
|
||||
return sendPlainText(c, http.StatusBadRequest, "Invalid request!")
|
||||
}
|
||||
|
||||
scheme := strings.ToLower(parsed.Scheme)
|
||||
if scheme != "http" && scheme != "https" {
|
||||
return sendPlainText(c, http.StatusBadRequest, "Invalid request!")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil)
|
||||
if err != nil {
|
||||
h.logger.WithError(err).Error("Failed to build remote request")
|
||||
return sendPlainText(c, http.StatusInternalServerError, "Failed to fetch remote content")
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
h.logger.WithError(err).Error("Failed to fetch remote content")
|
||||
return sendPlainText(c, http.StatusBadGateway, "Failed to fetch remote content")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
h.logger.WithError(err).Error("Failed to read remote response")
|
||||
return sendPlainText(c, http.StatusInternalServerError, "Failed to fetch remote content")
|
||||
}
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "text/plain; charset=utf-8"
|
||||
}
|
||||
c.Set("Content-Type", contentType)
|
||||
return c.Status(resp.StatusCode).Send(body)
|
||||
}
|
||||
|
||||
// HandleGetLocal 读取本地文件,兼容 /getlocal 端点
|
||||
func (h *ConversionHandler) HandleGetLocal(c *fiber.Ctx) error {
|
||||
h.logger.Info("Legacy getlocal request received")
|
||||
|
||||
pathParam := c.Query("path")
|
||||
if pathParam == "" {
|
||||
return sendPlainText(c, http.StatusBadRequest, "Invalid request!")
|
||||
}
|
||||
|
||||
resolved := h.resolvePath(pathParam)
|
||||
data, err := os.ReadFile(resolved)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return sendPlainText(c, http.StatusNotFound, "Not Found")
|
||||
}
|
||||
h.logger.WithError(err).Errorf("Failed to read local file: %s", resolved)
|
||||
return sendPlainText(c, http.StatusInternalServerError, "Failed to read file")
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/plain; charset=utf-8")
|
||||
return c.Status(http.StatusOK).Send(data)
|
||||
}
|
||||
|
||||
func (h *ConversionHandler) resolvePath(p string) string {
|
||||
if filepath.IsAbs(p) {
|
||||
return p
|
||||
}
|
||||
base := h.configMgr.GetBasePath()
|
||||
if base == "" {
|
||||
return p
|
||||
}
|
||||
return filepath.Join(base, p)
|
||||
}
|
||||
|
||||
func sendPlainText(c *fiber.Ctx, status int, body string) error {
|
||||
c.Set("Content-Type", "text/plain; charset=utf-8")
|
||||
return c.Status(status).SendString(body)
|
||||
}
|
||||
|
||||
var errProfileSectionMissing = errors.New("profile section missing")
|
||||
|
||||
func extractProfileToken(data []byte) (string, error) {
|
||||
scanner := bufio.NewScanner(bytes.NewReader(data))
|
||||
inProfile := false
|
||||
foundProfile := false
|
||||
token := ""
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
inProfile = strings.EqualFold(line, "[profile]")
|
||||
if inProfile {
|
||||
foundProfile = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !inProfile {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
if strings.EqualFold(key, "profile_token") {
|
||||
token = value
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !foundProfile {
|
||||
return "", errProfileSectionMissing
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func buildRequestOptions(req *conversion.ConversionRequest) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"url": req.SourceSummary(),
|
||||
"emoji": req.Emoji,
|
||||
"udp": req.UDP,
|
||||
"ipv6": req.IPv6,
|
||||
"insert": req.Insert,
|
||||
"strict": req.Strict,
|
||||
"list": req.List,
|
||||
"group": req.Group,
|
||||
"filename": req.Filename,
|
||||
"config": req.ConfigURL,
|
||||
"include": strings.Join(req.Include, ","),
|
||||
"exclude": strings.Join(req.Exclude, ","),
|
||||
"compatible": req.Compatible,
|
||||
"append_type": req.AppendType,
|
||||
"tfo": req.TFO,
|
||||
"script": req.Script,
|
||||
"scv": req.SkipCert,
|
||||
"fdn": req.FilterDeprecated,
|
||||
"expand": req.ExpandRules,
|
||||
"append_info": req.AppendInfo,
|
||||
"prepend": req.Prepend,
|
||||
"classic": req.Classic,
|
||||
"tls13": req.TLS13,
|
||||
"add_emoji": req.AddEmoji,
|
||||
"remove_emoji": req.RemoveEmoji,
|
||||
"upload": req.Upload,
|
||||
"upload_path": req.UploadPath,
|
||||
"rename": strings.Join(req.RenameRules, "`"),
|
||||
"filter_script": req.FilterScript,
|
||||
"dev_id": req.DeviceID,
|
||||
"interval": req.Interval,
|
||||
"groups": strings.Join(req.Groups, "@"),
|
||||
"ruleset": strings.Join(req.Rulesets, "@"),
|
||||
"providers": strings.Join(req.Providers, "@"),
|
||||
"emoji_rule": strings.Join(req.EmojiRules, "`"),
|
||||
}
|
||||
}
|
||||
|
||||
func resolveAutoTarget(userAgent string) string {
|
||||
ua := strings.ToLower(strings.TrimSpace(userAgent))
|
||||
if ua == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(ua, "surge"):
|
||||
return "surge"
|
||||
case strings.Contains(ua, "quantumult x"), strings.Contains(ua, "quanx"):
|
||||
return "quanx"
|
||||
case strings.Contains(ua, "loon"):
|
||||
return "loon"
|
||||
case strings.Contains(ua, "surfboard"):
|
||||
return "surfboard"
|
||||
case strings.Contains(ua, "clash"), strings.Contains(ua, "stash"), strings.Contains(ua, "shadowrocket"):
|
||||
return "clash"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// HandleGetRulesetLegacy 获取规则集内容,兼容 /getruleset 端点
|
||||
func (h *ConversionHandler) HandleGetRulesetLegacy(c *fiber.Ctx) error {
|
||||
h.logger.Info("Legacy getruleset request received")
|
||||
|
||||
encoded := c.Query("url")
|
||||
if encoded == "" {
|
||||
return sendPlainText(c, http.StatusBadRequest, "Invalid request!")
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
h.logger.WithError(err).Warn("Failed to decode ruleset url parameter")
|
||||
return sendPlainText(c, http.StatusBadRequest, "Invalid request!")
|
||||
}
|
||||
|
||||
entries := strings.Split(string(decoded), "|")
|
||||
var builder strings.Builder
|
||||
for _, entry := range entries {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(entry, ",", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
path := strings.TrimSpace(parts[1])
|
||||
resolved := h.resolvePath(path)
|
||||
content, err := os.ReadFile(resolved)
|
||||
if err != nil {
|
||||
h.logger.WithError(err).Warnf("Failed to read ruleset file: %s", resolved)
|
||||
continue
|
||||
}
|
||||
builder.Write(content)
|
||||
if len(content) > 0 && content[len(content)-1] != '\n' {
|
||||
builder.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
result := builder.String()
|
||||
if strings.TrimSpace(result) == "" {
|
||||
return sendPlainText(c, http.StatusNotFound, "Ruleset not found")
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/plain; charset=utf-8")
|
||||
return c.Status(http.StatusOK).SendString(result)
|
||||
}
|
||||
|
||||
// HandleGetProfile 获取配置文件,兼容 /getprofile 端点
|
||||
func (h *ConversionHandler) HandleGetProfile(c *fiber.Ctx) error {
|
||||
h.logger.Info("Legacy getprofile request received")
|
||||
|
||||
nameParam := c.Query("name")
|
||||
token := c.Query("token")
|
||||
if token == "" || nameParam == "" {
|
||||
return sendPlainText(c, http.StatusForbidden, "Forbidden")
|
||||
}
|
||||
|
||||
profiles := strings.Split(nameParam, "|")
|
||||
if len(profiles) == 0 || strings.TrimSpace(profiles[0]) == "" {
|
||||
return sendPlainText(c, http.StatusForbidden, "Forbidden")
|
||||
}
|
||||
|
||||
primaryPath := h.resolvePath(strings.TrimSpace(profiles[0]))
|
||||
data, err := os.ReadFile(primaryPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return sendPlainText(c, http.StatusNotFound, "Profile not found")
|
||||
}
|
||||
h.logger.WithError(err).Errorf("Failed to read profile %s", primaryPath)
|
||||
return sendPlainText(c, http.StatusInternalServerError, "Broken profile!")
|
||||
}
|
||||
|
||||
profileToken, err := extractProfileToken(data)
|
||||
if err != nil {
|
||||
if errors.Is(err, errProfileSectionMissing) {
|
||||
h.logger.Warnf("Profile %s missing [Profile] section", primaryPath)
|
||||
return sendPlainText(c, http.StatusForbidden, "Forbidden")
|
||||
}
|
||||
h.logger.WithError(err).Errorf("Failed to parse profile %s", primaryPath)
|
||||
return sendPlainText(c, http.StatusInternalServerError, "Broken profile!")
|
||||
}
|
||||
|
||||
if profileToken != "" {
|
||||
if token != profileToken {
|
||||
return sendPlainText(c, http.StatusForbidden, "Forbidden")
|
||||
}
|
||||
} else {
|
||||
security := h.configMgr.GetSecurityConfig()
|
||||
if len(security.AccessTokens) > 0 && token != security.AccessTokens[0] {
|
||||
return sendPlainText(c, http.StatusForbidden, "Forbidden")
|
||||
}
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/plain; charset=utf-8")
|
||||
return c.Status(http.StatusOK).Send(data)
|
||||
}
|
||||
212
internal/handler/health.go
Normal file
212
internal/handler/health.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/subconverter-go/internal/config"
|
||||
"github.com/subconverter-go/internal/logging"
|
||||
)
|
||||
|
||||
// HealthHandler 健康检查处理器
|
||||
// 提供应用程序健康状态检查功能
|
||||
type HealthHandler struct {
|
||||
logger *logging.Logger
|
||||
configMgr *config.ConfigManager
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// NewHealthHandler 创建新的健康检查处理器
|
||||
// 返回初始化好的HealthHandler实例
|
||||
func NewHealthHandler(logger *logging.Logger, configMgr *config.ConfigManager) *HealthHandler {
|
||||
return &HealthHandler{
|
||||
logger: logger,
|
||||
configMgr: configMgr,
|
||||
startTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// HandleHealth 处理健康检查请求
|
||||
// 返回应用程序的健康状态信息
|
||||
func (h *HealthHandler) HandleHealth(c *fiber.Ctx) error {
|
||||
h.logger.Debug("Health check request received")
|
||||
|
||||
// 获取配置信息
|
||||
config := h.configMgr.GetConfig()
|
||||
|
||||
// 计算运行时间
|
||||
uptime := time.Since(h.startTime).String()
|
||||
|
||||
// 创建健康状态响应
|
||||
health := map[string]interface{}{
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"uptime": uptime,
|
||||
"version": "1.0.0",
|
||||
"service": "subconverter-go",
|
||||
"environment": "development", // 可以从配置中获取
|
||||
}
|
||||
|
||||
// 添加服务器信息
|
||||
health["server"] = map[string]interface{}{
|
||||
"host": config.Server.Host,
|
||||
"port": config.Server.Port,
|
||||
"read_timeout": config.Server.ReadTimeout,
|
||||
"write_timeout": config.Server.WriteTimeout,
|
||||
"max_request_size": config.Server.MaxRequestSize,
|
||||
}
|
||||
|
||||
// 添加配置信息
|
||||
health["config"] = map[string]interface{}{
|
||||
"default_target": config.Conversion.DefaultTarget,
|
||||
"supported_targets": config.Conversion.SupportedTargets,
|
||||
"rate_limit": config.Security.RateLimit,
|
||||
"cors_origins": config.Security.CorsOrigins,
|
||||
}
|
||||
|
||||
// 添加系统信息
|
||||
health["system"] = map[string]interface{}{
|
||||
"memory_usage": "N/A", // 可以添加内存使用情况
|
||||
"cpu_usage": "N/A", // 可以添加CPU使用情况
|
||||
"goroutines": "N/A", // 可以添加goroutine数量
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
c.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
c.Set("Pragma", "no-cache")
|
||||
c.Set("Expires", "0")
|
||||
|
||||
return c.JSON(health)
|
||||
}
|
||||
|
||||
// HandleReady 处理就绪检查请求
|
||||
// 用于Kubernetes等系统的就绪探针
|
||||
func (h *HealthHandler) HandleReady(c *fiber.Ctx) error {
|
||||
h.logger.Debug("Readiness check request received")
|
||||
|
||||
// 检查配置是否已加载
|
||||
config := h.configMgr.GetConfig()
|
||||
if config == nil {
|
||||
h.logger.Warn("Configuration not loaded for readiness check")
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(map[string]interface{}{
|
||||
"status": "not ready",
|
||||
"message": "configuration not loaded",
|
||||
})
|
||||
}
|
||||
|
||||
// 检查必要的服务是否可用
|
||||
// 这里可以添加数据库连接、外部服务连接等检查
|
||||
|
||||
// 创建就绪状态响应
|
||||
ready := map[string]interface{}{
|
||||
"status": "ready",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"service": "subconverter-go",
|
||||
"checks": map[string]interface{}{
|
||||
"configuration": "ok",
|
||||
"server": "ok",
|
||||
"dependencies": "ok",
|
||||
},
|
||||
}
|
||||
|
||||
return c.JSON(ready)
|
||||
}
|
||||
|
||||
// HandleLive 处理存活检查请求
|
||||
// 用于Kubernetes等系统的存活探针
|
||||
func (h *HealthHandler) HandleLive(c *fiber.Ctx) error {
|
||||
h.logger.Debug("Liveness check request received")
|
||||
|
||||
// 简单的存活检查,如果应用程序能响应就认为存活
|
||||
live := map[string]interface{}{
|
||||
"status": "alive",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"service": "subconverter-go",
|
||||
}
|
||||
|
||||
return c.JSON(live)
|
||||
}
|
||||
|
||||
// HandleMetrics 处理指标请求
|
||||
// 返回应用程序的运行时指标
|
||||
func (h *HealthHandler) HandleMetrics(c *fiber.Ctx) error {
|
||||
h.logger.Debug("Metrics request received")
|
||||
|
||||
// 获取配置信息
|
||||
config := h.configMgr.GetConfig()
|
||||
|
||||
// 计算运行时间
|
||||
uptime := time.Since(h.startTime)
|
||||
|
||||
// 创建指标响应
|
||||
metrics := map[string]interface{}{
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"uptime_ms": uptime.Milliseconds(),
|
||||
"service": "subconverter-go",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
|
||||
// 添加服务器指标
|
||||
metrics["server"] = map[string]interface{}{
|
||||
"host": config.Server.Host,
|
||||
"port": config.Server.Port,
|
||||
"config": map[string]interface{}{
|
||||
"read_timeout": config.Server.ReadTimeout,
|
||||
"write_timeout": config.Server.WriteTimeout,
|
||||
"max_request_size": config.Server.MaxRequestSize,
|
||||
},
|
||||
}
|
||||
|
||||
// 添加转换相关指标
|
||||
metrics["conversion"] = map[string]interface{}{
|
||||
"default_target": config.Conversion.DefaultTarget,
|
||||
"supported_targets": config.Conversion.SupportedTargets,
|
||||
"default_emoji": config.Conversion.DefaultEmoji,
|
||||
"default_udp": config.Conversion.DefaultUDP,
|
||||
"max_nodes": config.Conversion.MaxNodes,
|
||||
"cache_timeout": config.Conversion.CacheTimeout,
|
||||
}
|
||||
|
||||
// 添加安全相关指标
|
||||
metrics["security"] = map[string]interface{}{
|
||||
"rate_limit": config.Security.RateLimit,
|
||||
"timeout": config.Security.Timeout,
|
||||
"cors_origins_count": len(config.Security.CorsOrigins),
|
||||
"access_tokens_count": len(config.Security.AccessTokens),
|
||||
}
|
||||
|
||||
// 添加日志相关指标
|
||||
metrics["logging"] = map[string]interface{}{
|
||||
"level": config.Logging.Level,
|
||||
"format": config.Logging.Format,
|
||||
"output": config.Logging.Output,
|
||||
}
|
||||
|
||||
return c.JSON(metrics)
|
||||
}
|
||||
|
||||
// HandlePing 处理ping请求
|
||||
// 用于网络连通性测试
|
||||
func (h *HealthHandler) HandlePing(c *fiber.Ctx) error {
|
||||
h.logger.Debug("Ping request received")
|
||||
|
||||
// 简单的ping响应
|
||||
ping := map[string]interface{}{
|
||||
"message": "pong",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"service": "subconverter-go",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
|
||||
return c.JSON(ping)
|
||||
}
|
||||
|
||||
// GetUptime 获取应用程序运行时间
|
||||
func (h *HealthHandler) GetUptime() time.Duration {
|
||||
return time.Since(h.startTime)
|
||||
}
|
||||
|
||||
// GetStartTime 获取应用程序启动时间
|
||||
func (h *HealthHandler) GetStartTime() time.Time {
|
||||
return h.startTime
|
||||
}
|
||||
801
internal/handler/template_handler.go
Normal file
801
internal/handler/template_handler.go
Normal file
@@ -0,0 +1,801 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/subconverter-go/internal/config"
|
||||
"github.com/subconverter-go/internal/logging"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// TemplateHandler 模板处理器
|
||||
type TemplateHandler struct {
|
||||
templateManager *config.TemplateManager
|
||||
logger *logging.Logger
|
||||
configManager *config.ConfigManager
|
||||
}
|
||||
|
||||
// TemplateInfo 模板信息
|
||||
type TemplateInfo struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// RulesetInfo 规则集信息
|
||||
type RulesetInfo struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
RuleCount int `json:"rule_count"`
|
||||
}
|
||||
|
||||
// TemplateListRequest 模板列表请求
|
||||
type TemplateListRequest struct {
|
||||
Type string `query:"type"` // 模板类型: clash, surge, quanx, loon, surfboard, v2ray
|
||||
Format string `query:"format"` // 格式: json, yaml
|
||||
Profile string `query:"profile"` // 配置文件名
|
||||
}
|
||||
|
||||
// TemplateRenderRequest 模板渲染请求
|
||||
type TemplateRenderRequest struct {
|
||||
Template string `json:"template"` // 模板名称
|
||||
Profile string `json:"profile"` // 配置文件名
|
||||
Variables map[string]interface{} `json:"variables"` // 自定义变量
|
||||
Target string `json:"target"` // 目标格式
|
||||
NodeData interface{} `json:"node_data"` // 节点数据
|
||||
GroupName string `json:"group_name"` // 组名
|
||||
}
|
||||
|
||||
// TemplateRenderResponse 模板渲染响应
|
||||
type TemplateRenderResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Content string `json:"content"`
|
||||
Variables map[string]interface{} `json:"variables"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
// NewTemplateHandler 创建模板处理器
|
||||
func NewTemplateHandler(templateManager *config.TemplateManager, logger *logging.Logger, configManager *config.ConfigManager) *TemplateHandler {
|
||||
return &TemplateHandler{
|
||||
templateManager: templateManager,
|
||||
logger: logger,
|
||||
configManager: configManager,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func (h *TemplateHandler) RegisterRoutes(app *fiber.App) {
|
||||
// 原项目兼容性路由 - /render 端点
|
||||
app.Get("/render", h.RenderDirect)
|
||||
|
||||
// 模板相关路由
|
||||
app.Get("/api/templates", h.ListTemplates)
|
||||
app.Get("/api/templates/:name", h.GetTemplate)
|
||||
app.Post("/api/templates/render", h.RenderTemplate)
|
||||
app.Get("/api/templates/:name/preview", h.PreviewTemplate)
|
||||
|
||||
// 规则集相关路由
|
||||
app.Get("/api/rulesets", h.ListRulesets)
|
||||
app.Get("/api/rulesets/:name", h.GetRuleset)
|
||||
app.Get("/api/rulesets/:name/content", h.GetRulesetContent)
|
||||
|
||||
// 配置文件相关路由
|
||||
app.Get("/api/profiles", h.ListProfiles)
|
||||
app.Get("/api/profiles/:name", h.GetProfile)
|
||||
app.Post("/api/profiles/:name/apply", h.ApplyProfile)
|
||||
|
||||
// 代码片段相关路由
|
||||
app.Get("/api/snippets", h.ListSnippets)
|
||||
app.Get("/api/snippets/:name", h.GetSnippet)
|
||||
|
||||
// 模板管理路由
|
||||
app.Post("/api/templates/reload", h.ReloadTemplates)
|
||||
app.Get("/api/templates/info", h.GetTemplatesInfo)
|
||||
}
|
||||
|
||||
// RenderDirect 直接渲染模板(原项目兼容性端点)
|
||||
func (h *TemplateHandler) RenderDirect(c *fiber.Ctx) error {
|
||||
path := c.Query("path")
|
||||
if path == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "path parameter is required",
|
||||
})
|
||||
}
|
||||
|
||||
// 验证路径安全性,防止目录遍历攻击
|
||||
if strings.Contains(path, "..") || strings.HasPrefix(path, "/") {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Invalid path",
|
||||
})
|
||||
}
|
||||
|
||||
// 从模板路径中提取模板名称
|
||||
templateName := strings.TrimSuffix(path, filepath.Ext(path))
|
||||
|
||||
// 检查模板是否存在
|
||||
if _, exists := h.templateManager.GetTemplate(templateName); !exists {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": fmt.Sprintf("Template '%s' not found", templateName),
|
||||
})
|
||||
}
|
||||
|
||||
// 准备模板变量,从URL参数中获取
|
||||
variables := h.prepareTemplateVariables(c)
|
||||
|
||||
// 渲染模板
|
||||
content, err := h.templateManager.RenderTemplate(templateName, variables)
|
||||
if err != nil {
|
||||
h.logger.Errorf("Failed to render template %s: %v", templateName, err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": fmt.Sprintf("Failed to render template: %v", err),
|
||||
})
|
||||
}
|
||||
|
||||
// 根据模板类型设置Content-Type
|
||||
contentType := h.getContentType(templateName)
|
||||
c.Set("Content-Type", contentType)
|
||||
|
||||
return c.SendString(content)
|
||||
}
|
||||
|
||||
// prepareTemplateVariables 准备模板变量
|
||||
func (h *TemplateHandler) prepareTemplateVariables(c *fiber.Ctx) config.TemplateVariables {
|
||||
// 从查询参数中提取模板变量
|
||||
requestConfig := config.RequestConfig{
|
||||
Target: c.Query("target", "clash"),
|
||||
Clash: make(map[string]interface{}),
|
||||
Surge: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
// 处理额外的查询参数
|
||||
for key, value := range c.Queries() {
|
||||
switch {
|
||||
case strings.HasPrefix(key, "clash."):
|
||||
paramName := strings.TrimPrefix(key, "clash.")
|
||||
requestConfig.Clash[paramName] = value
|
||||
case strings.HasPrefix(key, "surge."):
|
||||
paramName := strings.TrimPrefix(key, "surge.")
|
||||
requestConfig.Surge[paramName] = value
|
||||
}
|
||||
}
|
||||
|
||||
localConfig := config.LocalConfig{
|
||||
Clash: make(map[string]interface{}),
|
||||
Surge: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
// 获取全局配置
|
||||
globalConfig := config.TemplateConfig{
|
||||
Clash: config.ClashConfig{
|
||||
HTTPPort: 7890,
|
||||
SocksPort: 7891,
|
||||
AllowLAN: true,
|
||||
LogLevel: "info",
|
||||
ExternalController: "127.0.0.1:9090",
|
||||
},
|
||||
Surge: config.SurgeConfig{
|
||||
Port: 8080,
|
||||
ProxyHTTPPort: 8081,
|
||||
ProxySOCKS5Port: 8082,
|
||||
MixedPort: 8080,
|
||||
AllowLAN: true,
|
||||
LogLevel: "info",
|
||||
},
|
||||
// 可以添加其他客户端的默认配置
|
||||
}
|
||||
|
||||
return config.TemplateVariables{
|
||||
Global: globalConfig,
|
||||
NodeInfo: h.getSampleNodeData(),
|
||||
GroupName: c.Query("group", "Proxy"),
|
||||
UpdateTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
UserInfo: "",
|
||||
TotalNodes: 1,
|
||||
Request: requestConfig,
|
||||
Local: localConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// ListTemplates 列出所有模板
|
||||
func (h *TemplateHandler) ListTemplates(c *fiber.Ctx) error {
|
||||
var req TemplateListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Invalid request parameters",
|
||||
})
|
||||
}
|
||||
|
||||
templates := h.templateManager.ListTemplates()
|
||||
var filteredTemplates []TemplateInfo
|
||||
|
||||
for _, name := range templates {
|
||||
// 根据类型过滤
|
||||
if req.Type != "" {
|
||||
if !h.isTemplateOfType(name, req.Type) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
templateInfo := TemplateInfo{
|
||||
Name: name,
|
||||
Type: h.getTemplateType(name),
|
||||
Description: h.getTemplateDescription(name),
|
||||
}
|
||||
|
||||
// 获取模板文件大小
|
||||
if content, err := h.templateManager.RenderTemplate(name, config.TemplateVariables{}); err == nil {
|
||||
templateInfo.Size = int64(len(content))
|
||||
}
|
||||
|
||||
filteredTemplates = append(filteredTemplates, templateInfo)
|
||||
}
|
||||
|
||||
// 根据格式返回不同响应
|
||||
if req.Format == "yaml" {
|
||||
return c.JSON(filteredTemplates)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"templates": filteredTemplates,
|
||||
"total": len(filteredTemplates),
|
||||
})
|
||||
}
|
||||
|
||||
// GetTemplate 获取特定模板
|
||||
func (h *TemplateHandler) GetTemplate(c *fiber.Ctx) error {
|
||||
name := c.Params("name")
|
||||
if name == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Template name is required",
|
||||
})
|
||||
}
|
||||
|
||||
_, exists := h.templateManager.GetTemplate(name)
|
||||
if !exists {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Template not found",
|
||||
})
|
||||
}
|
||||
|
||||
// 返回模板信息
|
||||
templateInfo := fiber.Map{
|
||||
"name": name,
|
||||
"type": h.getTemplateType(name),
|
||||
"description": h.getTemplateDescription(name),
|
||||
"exists": true,
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"template": templateInfo,
|
||||
})
|
||||
}
|
||||
|
||||
// RenderTemplate 渲染模板
|
||||
func (h *TemplateHandler) RenderTemplate(c *fiber.Ctx) error {
|
||||
var req TemplateRenderRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Invalid request body",
|
||||
})
|
||||
}
|
||||
|
||||
// 验证必需参数
|
||||
if req.Template == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Template name is required",
|
||||
})
|
||||
}
|
||||
|
||||
// 准备模板变量
|
||||
variables := config.TemplateVariables{
|
||||
GroupName: req.GroupName,
|
||||
NodeInfo: req.NodeData,
|
||||
TotalNodes: h.getTotalNodes(req.NodeData),
|
||||
}
|
||||
|
||||
// 应用自定义变量
|
||||
if req.Variables != nil {
|
||||
if global, ok := req.Variables["global"]; ok {
|
||||
if globalMap, ok := global.(map[string]interface{}); ok {
|
||||
variables.Global = h.parseGlobalConfig(globalMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染模板
|
||||
content, err := h.templateManager.RenderTemplateWithDefaults(
|
||||
req.Template,
|
||||
variables.GroupName,
|
||||
variables.NodeInfo,
|
||||
variables.TotalNodes,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
h.logger.Errorf("Failed to render template %s: %v", req.Template, err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(TemplateRenderResponse{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("Failed to render template: %v", err),
|
||||
})
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
response := TemplateRenderResponse{
|
||||
Success: true,
|
||||
Content: content,
|
||||
Variables: map[string]interface{}{
|
||||
"group_name": variables.GroupName,
|
||||
"total_nodes": variables.TotalNodes,
|
||||
"template": req.Template,
|
||||
},
|
||||
Metadata: map[string]interface{}{
|
||||
"template_name": req.Template,
|
||||
"target": req.Target,
|
||||
"group_name": req.GroupName,
|
||||
"node_count": variables.TotalNodes,
|
||||
},
|
||||
}
|
||||
|
||||
return c.JSON(response)
|
||||
}
|
||||
|
||||
// PreviewTemplate 预览模板
|
||||
func (h *TemplateHandler) PreviewTemplate(c *fiber.Ctx) error {
|
||||
name := c.Params("name")
|
||||
if name == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Template name is required",
|
||||
})
|
||||
}
|
||||
|
||||
// 使用示例数据预览
|
||||
sampleData := h.getSampleNodeData()
|
||||
|
||||
content, err := h.templateManager.RenderTemplateWithDefaults(
|
||||
name,
|
||||
"SampleGroup",
|
||||
sampleData,
|
||||
1,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
h.logger.Errorf("Failed to preview template %s: %v", name, err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": fmt.Sprintf("Failed to preview template: %v", err),
|
||||
})
|
||||
}
|
||||
|
||||
// 根据模板类型设置Content-Type
|
||||
contentType := h.getContentType(name)
|
||||
c.Set("Content-Type", contentType)
|
||||
|
||||
return c.SendString(content)
|
||||
}
|
||||
|
||||
// ListRulesets 列出所有规则集
|
||||
func (h *TemplateHandler) ListRulesets(c *fiber.Ctx) error {
|
||||
rulesets := h.templateManager.ListRulesets()
|
||||
var rulesetInfos []RulesetInfo
|
||||
|
||||
for _, name := range rulesets {
|
||||
if content, exists := h.templateManager.GetRuleset(name); exists {
|
||||
rulesetInfos = append(rulesetInfos, RulesetInfo{
|
||||
Name: name,
|
||||
Type: h.getRulesetType(name),
|
||||
Description: h.getRulesetDescription(name),
|
||||
RuleCount: h.countRules(content),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"rulesets": rulesetInfos,
|
||||
"total": len(rulesetInfos),
|
||||
})
|
||||
}
|
||||
|
||||
// GetRuleset 获取规则集信息
|
||||
func (h *TemplateHandler) GetRuleset(c *fiber.Ctx) error {
|
||||
name := c.Params("name")
|
||||
if name == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Ruleset name is required",
|
||||
})
|
||||
}
|
||||
|
||||
content, exists := h.templateManager.GetRuleset(name)
|
||||
if !exists {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Ruleset not found",
|
||||
})
|
||||
}
|
||||
|
||||
rulesetInfo := RulesetInfo{
|
||||
Name: name,
|
||||
Type: h.getRulesetType(name),
|
||||
Description: h.getRulesetDescription(name),
|
||||
RuleCount: h.countRules(content),
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"ruleset": rulesetInfo,
|
||||
})
|
||||
}
|
||||
|
||||
// GetRulesetContent 获取规则集内容
|
||||
func (h *TemplateHandler) GetRulesetContent(c *fiber.Ctx) error {
|
||||
name := c.Params("name")
|
||||
if name == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Ruleset name is required",
|
||||
})
|
||||
}
|
||||
|
||||
content, exists := h.templateManager.GetRuleset(name)
|
||||
if !exists {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Ruleset not found",
|
||||
})
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/plain")
|
||||
return c.SendString(content)
|
||||
}
|
||||
|
||||
// ListProfiles 列出所有配置文件
|
||||
func (h *TemplateHandler) ListProfiles(c *fiber.Ctx) error {
|
||||
profiles := h.templateManager.ListProfileConfigs()
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"profiles": profiles,
|
||||
"total": len(profiles),
|
||||
})
|
||||
}
|
||||
|
||||
// GetProfile 获取配置文件
|
||||
func (h *TemplateHandler) GetProfile(c *fiber.Ctx) error {
|
||||
name := c.Params("name")
|
||||
if name == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Profile name is required",
|
||||
})
|
||||
}
|
||||
|
||||
profile, exists := h.templateManager.GetProfileConfig(name)
|
||||
if !exists {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Profile not found",
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"profile": profile,
|
||||
})
|
||||
}
|
||||
|
||||
// ApplyProfile 应用配置文件
|
||||
func (h *TemplateHandler) ApplyProfile(c *fiber.Ctx) error {
|
||||
name := c.Params("name")
|
||||
if name == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Profile name is required",
|
||||
})
|
||||
}
|
||||
|
||||
// 这里简化处理,实际应该解析并应用配置文件
|
||||
_, exists := h.templateManager.GetProfileConfig(name)
|
||||
if !exists {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Profile not found",
|
||||
})
|
||||
}
|
||||
|
||||
h.logger.Infof("Applied profile: %s", name)
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf("Profile '%s' applied successfully", name),
|
||||
})
|
||||
}
|
||||
|
||||
// ListSnippets 列出所有代码片段
|
||||
func (h *TemplateHandler) ListSnippets(c *fiber.Ctx) error {
|
||||
snippets := make(map[string]string)
|
||||
|
||||
// 这里简化处理,实际应该从templateManager获取
|
||||
snippets["groups.txt"] = "Proxy group configuration snippets"
|
||||
snippets["rename_node.txt"] = "Node renaming snippets"
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"snippets": snippets,
|
||||
"total": len(snippets),
|
||||
})
|
||||
}
|
||||
|
||||
// GetSnippet 获取代码片段
|
||||
func (h *TemplateHandler) GetSnippet(c *fiber.Ctx) error {
|
||||
name := c.Params("name")
|
||||
if name == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Snippet name is required",
|
||||
})
|
||||
}
|
||||
|
||||
snippet, exists := h.templateManager.GetSnippet(name)
|
||||
if !exists {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": "Snippet not found",
|
||||
})
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/plain")
|
||||
return c.SendString(snippet)
|
||||
}
|
||||
|
||||
// ReloadTemplates 重新加载模板
|
||||
func (h *TemplateHandler) ReloadTemplates(c *fiber.Ctx) error {
|
||||
if err := h.templateManager.Reload(); err != nil {
|
||||
h.logger.Errorf("Failed to reload templates: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"error": fmt.Sprintf("Failed to reload templates: %v", err),
|
||||
})
|
||||
}
|
||||
|
||||
h.logger.Info("Templates reloaded successfully")
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"message": "Templates reloaded successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetTemplatesInfo 获取模板系统信息
|
||||
func (h *TemplateHandler) GetTemplatesInfo(c *fiber.Ctx) error {
|
||||
templates := h.templateManager.ListTemplates()
|
||||
rulesets := h.templateManager.ListRulesets()
|
||||
profiles := h.templateManager.ListProfileConfigs()
|
||||
|
||||
info := fiber.Map{
|
||||
"template_count": len(templates),
|
||||
"ruleset_count": len(rulesets),
|
||||
"profile_count": len(profiles),
|
||||
"base_path": h.configManager.GetBasePath(),
|
||||
"supported_types": []string{"clash", "surge", "quanx", "loon", "surfboard", "v2ray"},
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"info": info,
|
||||
})
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
|
||||
// isTemplateOfType 检查模板是否为指定类型
|
||||
func (h *TemplateHandler) isTemplateOfType(name, templateType string) bool {
|
||||
return strings.Contains(strings.ToLower(name), strings.ToLower(templateType))
|
||||
}
|
||||
|
||||
// getTemplateType 获取模板类型
|
||||
func (h *TemplateHandler) getTemplateType(name string) string {
|
||||
name = strings.ToLower(name)
|
||||
switch {
|
||||
case strings.Contains(name, "clash"):
|
||||
return "clash"
|
||||
case strings.Contains(name, "surge"):
|
||||
return "surge"
|
||||
case strings.Contains(name, "quanx"):
|
||||
return "quanx"
|
||||
case strings.Contains(name, "loon"):
|
||||
return "loon"
|
||||
case strings.Contains(name, "surfboard"):
|
||||
return "surfboard"
|
||||
case strings.Contains(name, "v2ray"):
|
||||
return "v2ray"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// getTemplateDescription 获取模板描述
|
||||
func (h *TemplateHandler) getTemplateDescription(name string) string {
|
||||
descriptions := map[string]string{
|
||||
"GeneralClashConfig": "General Clash configuration template",
|
||||
"forcerule": "Force rule configuration template",
|
||||
"surge.conf": "Surge configuration template",
|
||||
"quan.conf": "Quantumult configuration template",
|
||||
"loon.conf": "Loon configuration template",
|
||||
"surfboard.conf": "Surfboard configuration template",
|
||||
}
|
||||
|
||||
if desc, exists := descriptions[name]; exists {
|
||||
return desc
|
||||
}
|
||||
return fmt.Sprintf("Template: %s", name)
|
||||
}
|
||||
|
||||
// getRulesetType 获取规则集类型
|
||||
func (h *TemplateHandler) getRulesetType(name string) string {
|
||||
ext := filepath.Ext(name)
|
||||
switch ext {
|
||||
case ".list":
|
||||
return "domain_list"
|
||||
case ".txt":
|
||||
return "text_rules"
|
||||
case ".rules":
|
||||
return "rules"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// getRulesetDescription 获取规则集描述
|
||||
func (h *TemplateHandler) getRulesetDescription(name string) string {
|
||||
// 这里可以根据规则集名称返回描述
|
||||
return fmt.Sprintf("Ruleset: %s", name)
|
||||
}
|
||||
|
||||
// countRules 计算规则数量
|
||||
func (h *TemplateHandler) countRules(content string) int {
|
||||
lines := strings.Split(content, "\n")
|
||||
count := 0
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" && !strings.HasPrefix(line, "#") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// parseGlobalConfig 解析全局配置
|
||||
func (h *TemplateHandler) parseGlobalConfig(cfg map[string]interface{}) config.TemplateConfig {
|
||||
var globalConfig config.TemplateConfig
|
||||
|
||||
// 解析Clash配置
|
||||
if clash, ok := cfg["clash"].(map[string]interface{}); ok {
|
||||
globalConfig.Clash.HTTPPort = h.getInt(clash, "http_port", 7890)
|
||||
globalConfig.Clash.SocksPort = h.getInt(clash, "socks_port", 7891)
|
||||
globalConfig.Clash.AllowLAN = h.getBool(clash, "allow_lan", true)
|
||||
globalConfig.Clash.LogLevel = h.getString(clash, "log_level", "info")
|
||||
globalConfig.Clash.ExternalController = h.getString(clash, "external_controller", "127.0.0.1:9090")
|
||||
}
|
||||
|
||||
// 解析Surge配置
|
||||
if surge, ok := cfg["surge"].(map[string]interface{}); ok {
|
||||
globalConfig.Surge.Port = h.getInt(surge, "port", 8080)
|
||||
globalConfig.Surge.ProxyHTTPPort = h.getInt(surge, "proxy_http_port", 8081)
|
||||
globalConfig.Surge.ProxySOCKS5Port = h.getInt(surge, "proxy_socks5_port", 8082)
|
||||
globalConfig.Surge.MixedPort = h.getInt(surge, "mixed_port", 8080)
|
||||
globalConfig.Surge.AllowLAN = h.getBool(surge, "allow_lan", true)
|
||||
globalConfig.Surge.LogLevel = h.getString(surge, "log_level", "info")
|
||||
}
|
||||
|
||||
return globalConfig
|
||||
}
|
||||
|
||||
// getInt 获取整数值
|
||||
func (h *TemplateHandler) getInt(m map[string]interface{}, key string, defaultValue int) int {
|
||||
if val, ok := m[key]; ok {
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
return int(v)
|
||||
case int:
|
||||
return v
|
||||
case string:
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// getBool 获取布尔值
|
||||
func (h *TemplateHandler) getBool(m map[string]interface{}, key string, defaultValue bool) bool {
|
||||
if val, ok := m[key]; ok {
|
||||
switch v := val.(type) {
|
||||
case bool:
|
||||
return v
|
||||
case string:
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
return b
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// getString 获取字符串值
|
||||
func (h *TemplateHandler) getString(m map[string]interface{}, key string, defaultValue string) string {
|
||||
if val, ok := m[key]; ok {
|
||||
if str, ok := val.(string); ok {
|
||||
return str
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// getTotalNodes 获取节点总数
|
||||
func (h *TemplateHandler) getTotalNodes(nodeData interface{}) int {
|
||||
if nodeData == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 根据不同的数据结构计算节点数量
|
||||
switch data := nodeData.(type) {
|
||||
case []interface{}:
|
||||
return len(data)
|
||||
case map[string]interface{}:
|
||||
if proxies, ok := data["proxies"].([]interface{}); ok {
|
||||
return len(proxies)
|
||||
}
|
||||
}
|
||||
|
||||
return 1 // 默认返回1
|
||||
}
|
||||
|
||||
// getSampleNodeData 获取示例节点数据
|
||||
func (h *TemplateHandler) getSampleNodeData() interface{} {
|
||||
return map[string]interface{}{
|
||||
"remark": "Sample Server",
|
||||
"hostname": "sample.example.com",
|
||||
"port": 443,
|
||||
"type": "shadowsocks",
|
||||
"settings": map[string]interface{}{
|
||||
"method": "aes-256-gcm",
|
||||
"password": "sample-password",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getContentType 获取内容类型
|
||||
func (h *TemplateHandler) getContentType(name string) string {
|
||||
ext := filepath.Ext(name)
|
||||
switch ext {
|
||||
case ".yml", ".yaml":
|
||||
return "application/x-yaml"
|
||||
case ".json":
|
||||
return "application/json"
|
||||
case ".conf":
|
||||
return "text/plain"
|
||||
default:
|
||||
return "text/plain"
|
||||
}
|
||||
}
|
||||
289
internal/handler/version.go
Normal file
289
internal/handler/version.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/subconverter-go/internal/config"
|
||||
"github.com/subconverter-go/internal/logging"
|
||||
)
|
||||
|
||||
// VersionHandler 版本信息处理器
|
||||
// 提供应用程序版本和构建信息
|
||||
type VersionHandler struct {
|
||||
logger *logging.Logger
|
||||
configMgr *config.ConfigManager
|
||||
buildTime time.Time
|
||||
gitCommit string
|
||||
gitVersion string
|
||||
}
|
||||
|
||||
// NewVersionHandler 创建新的版本信息处理器
|
||||
// 返回初始化好的VersionHandler实例
|
||||
func NewVersionHandler(logger *logging.Logger, configMgr *config.ConfigManager) *VersionHandler {
|
||||
return &VersionHandler{
|
||||
logger: logger,
|
||||
configMgr: configMgr,
|
||||
buildTime: getBuildTime(),
|
||||
gitCommit: getGitCommit(),
|
||||
gitVersion: getGitVersion(),
|
||||
}
|
||||
}
|
||||
|
||||
// HandleVersion 处理版本信息请求
|
||||
// 返回详细的版本和构建信息
|
||||
func (h *VersionHandler) HandleVersion(c *fiber.Ctx) error {
|
||||
h.logger.Debug("Version info request received")
|
||||
|
||||
// 获取配置信息
|
||||
config := h.configMgr.GetConfig()
|
||||
|
||||
// 创建版本信息响应
|
||||
version := map[string]interface{}{
|
||||
"service": "subconverter-go",
|
||||
"version": "1.0.0",
|
||||
"description": "Proxy subscription converter service",
|
||||
"author": "subconverter-go team",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/subconverter-go/subconverter-go",
|
||||
"repository": "https://github.com/subconverter-go/subconverter-go.git",
|
||||
}
|
||||
|
||||
// 添加构建信息
|
||||
version["build"] = map[string]interface{}{
|
||||
"build_time": h.buildTime.Format(time.RFC3339),
|
||||
"git_commit": h.gitCommit,
|
||||
"git_version": h.gitVersion,
|
||||
"go_version": runtime.Version(),
|
||||
"compiler": runtime.Compiler,
|
||||
"platform": fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||
}
|
||||
|
||||
// 添加运行时信息
|
||||
version["runtime"] = map[string]interface{}{
|
||||
"goroutines": runtime.NumGoroutine(),
|
||||
"cgocalls": runtime.NumCgoCall(),
|
||||
"memory": "N/A", // 可以添加内存使用情况
|
||||
}
|
||||
|
||||
// 添加功能特性信息
|
||||
version["features"] = map[string]interface{}{
|
||||
"supported_formats": config.Conversion.SupportedTargets,
|
||||
"security": map[string]interface{}{
|
||||
"rate_limiting": config.Security.RateLimit > 0,
|
||||
"cors_enabled": len(config.Security.CorsOrigins) > 0,
|
||||
"auth_enabled": len(config.Security.AccessTokens) > 0,
|
||||
},
|
||||
"logging": map[string]interface{}{
|
||||
"level": config.Logging.Level,
|
||||
"format": config.Logging.Format,
|
||||
"output": config.Logging.Output,
|
||||
},
|
||||
"server": map[string]interface{}{
|
||||
"host": config.Server.Host,
|
||||
"port": config.Server.Port,
|
||||
"framework": "fiber/v2",
|
||||
},
|
||||
}
|
||||
|
||||
// 添加API信息
|
||||
version["api"] = map[string]interface{}{
|
||||
"version": "v1",
|
||||
"endpoints": []string{
|
||||
"GET /health - Health check",
|
||||
"GET /version - Version information",
|
||||
"GET /ready - Readiness check",
|
||||
"GET /live - Liveness check",
|
||||
"GET /metrics - Runtime metrics",
|
||||
"GET /ping - Connectivity test",
|
||||
"GET /sub - Subscription conversion",
|
||||
"POST /sub - Subscription conversion",
|
||||
"GET /api/targets - Supported targets",
|
||||
"POST /api/convert - Convert subscription",
|
||||
"GET /api/validate - Validate URL",
|
||||
},
|
||||
}
|
||||
|
||||
// 添加依赖信息
|
||||
version["dependencies"] = map[string]interface{}{
|
||||
"fiber": map[string]interface{}{
|
||||
"version": "v2.52.9",
|
||||
"website": "https://gofiber.io",
|
||||
},
|
||||
"viper": map[string]interface{}{
|
||||
"version": "v1.21.0",
|
||||
"website": "https://github.com/spf13/viper",
|
||||
},
|
||||
"cobra": map[string]interface{}{
|
||||
"version": "v1.10.1",
|
||||
"website": "https://github.com/spf13/cobra",
|
||||
},
|
||||
"logrus": map[string]interface{}{
|
||||
"version": "v1.8.1",
|
||||
"website": "https://github.com/sirupsen/logrus",
|
||||
},
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
c.Set("Cache-Control", "public, max-age=3600") // 缓存1小时
|
||||
c.Set("X-API-Version", "v1")
|
||||
c.Set("X-Service-Version", "1.0.0")
|
||||
|
||||
format := c.Query("format")
|
||||
accept := c.Get("Accept")
|
||||
if strings.EqualFold(format, "plain") || strings.Contains(accept, "text/plain") {
|
||||
plain := fmt.Sprintf("%s %s backend\n", version["service"], version["version"])
|
||||
c.Set("Content-Type", "text/plain; charset=utf-8")
|
||||
return c.Status(http.StatusOK).SendString(plain)
|
||||
}
|
||||
|
||||
return c.JSON(version)
|
||||
}
|
||||
|
||||
// HandleCompatibility 处理兼容性信息请求
|
||||
// 返回与原C++版本的兼容性信息
|
||||
func (h *VersionHandler) HandleCompatibility(c *fiber.Ctx) error {
|
||||
h.logger.Debug("Compatibility info request received")
|
||||
|
||||
// 创建兼容性信息响应
|
||||
compatibility := map[string]interface{}{
|
||||
"original_version": "subconverter (C++)",
|
||||
"go_version": "subconverter-go",
|
||||
"compatibility": "1:1 functional compatibility",
|
||||
"status": "in development",
|
||||
}
|
||||
|
||||
// 添加支持的格式兼容性
|
||||
compatibility["format_compatibility"] = map[string]interface{}{
|
||||
"input_formats": []string{
|
||||
"Shadowsocks (SS)",
|
||||
"ShadowsocksR (SSR)",
|
||||
"VMess",
|
||||
"Trojan",
|
||||
"HTTP/Socks5",
|
||||
"Telegram links",
|
||||
},
|
||||
"output_formats": []string{
|
||||
"Clash",
|
||||
"ClashR",
|
||||
"Quantumult",
|
||||
"Quantumult X",
|
||||
"Loon",
|
||||
"Surfboard",
|
||||
"Surge (v2, v3, v4)",
|
||||
"V2Ray",
|
||||
},
|
||||
"parameter_compatibility": "100%",
|
||||
}
|
||||
|
||||
// 添加API兼容性
|
||||
compatibility["api_compatibility"] = map[string]interface{}{
|
||||
"endpoints": []string{
|
||||
"/sub",
|
||||
"/config",
|
||||
"/health",
|
||||
"/version",
|
||||
},
|
||||
"parameters": []string{
|
||||
"target",
|
||||
"url",
|
||||
"emoji",
|
||||
"udp",
|
||||
"include",
|
||||
"exclude",
|
||||
"group",
|
||||
"config",
|
||||
},
|
||||
}
|
||||
|
||||
// 添加配置文件兼容性
|
||||
compatibility["config_compatibility"] = map[string]interface{}{
|
||||
"supported": "INI format",
|
||||
"structure": "Compatible with original",
|
||||
"sections": []string{
|
||||
"common",
|
||||
"node_pref",
|
||||
"userinfo",
|
||||
"managed_config",
|
||||
"server",
|
||||
"ruleset",
|
||||
},
|
||||
}
|
||||
|
||||
return c.JSON(compatibility)
|
||||
}
|
||||
|
||||
// HandleChangelog 处理变更日志请求
|
||||
// 返回版本变更历史
|
||||
func (h *VersionHandler) HandleChangelog(c *fiber.Ctx) error {
|
||||
h.logger.Debug("Changelog request received")
|
||||
|
||||
// 创建变更日志响应
|
||||
changelog := map[string]interface{}{
|
||||
"current_version": "1.0.0",
|
||||
"latest_release": "2024-01-01",
|
||||
"changes": []map[string]interface{}{
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"date": "2024-01-01",
|
||||
"type": "initial",
|
||||
"description": "Initial Go release with 1:1 compatibility to C++ version",
|
||||
"features": []string{
|
||||
"Complete conversion functionality",
|
||||
"HTTP API server",
|
||||
"CLI interface",
|
||||
"Configuration management",
|
||||
"Health checks and metrics",
|
||||
},
|
||||
"changes": []string{
|
||||
"Rewritten in Go language",
|
||||
"Improved error handling",
|
||||
"Better logging system",
|
||||
"Enhanced configuration management",
|
||||
},
|
||||
},
|
||||
},
|
||||
"upcoming": []map[string]interface{}{
|
||||
{
|
||||
"version": "1.1.0",
|
||||
"status": "planned",
|
||||
"features": []string{
|
||||
"Performance optimizations",
|
||||
"Enhanced caching",
|
||||
"Additional proxy format support",
|
||||
"Web interface",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return c.JSON(changelog)
|
||||
}
|
||||
|
||||
// getBuildTime 获取构建时间
|
||||
// 从编译时变量或当前时间获取
|
||||
func getBuildTime() time.Time {
|
||||
// 在实际项目中,这里应该从编译时变量获取
|
||||
// 例如: return time.Unix(buildTime, 0)
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// getGitCommit 获取Git提交信息
|
||||
// 从编译时变量获取
|
||||
func getGitCommit() string {
|
||||
// 在实际项目中,这里应该从编译时变量获取
|
||||
// 例如: return gitCommit
|
||||
return "development"
|
||||
}
|
||||
|
||||
// getGitVersion 获取Git版本信息
|
||||
// 从编译时变量获取
|
||||
func getGitVersion() string {
|
||||
// 在实际项目中,这里应该从编译时变量获取
|
||||
// 例如: return gitVersion
|
||||
return "v1.0.0-dev"
|
||||
}
|
||||
Reference in New Issue
Block a user