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