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

This commit is contained in:
Rogee
2025-09-28 10:05:07 +08:00
commit 7fcabe0225
481 changed files with 125127 additions and 0 deletions

View 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
View 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
}

View 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
View 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"
}