package conversion import ( "encoding/base64" "encoding/json" "fmt" "net/http" "net/url" "regexp" "strconv" "strings" ) // ConversionRequest 表示转换请求的参数和配置 // 该结构体包含从HTTP请求解析出的所有转换参数 type ConversionRequest struct { // 基本参数 Target string `json:"target"` // 目标格式:clash, surge, quanx, loon, surfboard, v2ray URL string `json:"url"` // 源订阅URL RawURL string `json:"-"` // 原始请求中的URL参数(可能包含多源) Sources []string `json:"-"` // 解析后的订阅源列表 // 请求上下文 UserAgent string `json:"-"` // 客户端传入的 User-Agent,用于远程拉取订阅时复用 // 可选参数 ConfigURL string `json:"config_url,omitempty"` // 自定义配置文件URL Emoji bool `json:"emoji,omitempty"` // 是否启用emoji UDP bool `json:"udp,omitempty"` // 是否启用UDP支持 IPv6 bool `json:"ipv6,omitempty"` // 是否启用IPv6支持 Group string `json:"group,omitempty"` // 节点分组名称 Insert bool `json:"insert,omitempty"` // 是否插入URL测试 Strict bool `json:"strict,omitempty"` // 是否严格模式 Compatible bool `json:"compatible,omitempty"` // 是否兼容模式 AppendType bool `json:"append_type,omitempty"` TFO bool `json:"tfo,omitempty"` Script bool `json:"script,omitempty"` SkipCert bool `json:"skip_cert_verify,omitempty"` FilterDeprecated bool `json:"filter_deprecated,omitempty"` ExpandRules bool `json:"expand_rules,omitempty"` AppendInfo bool `json:"append_info,omitempty"` Prepend bool `json:"prepend,omitempty"` Classic bool `json:"classic,omitempty"` TLS13 bool `json:"tls13,omitempty"` AddEmoji bool `json:"add_emoji,omitempty"` RemoveEmoji bool `json:"remove_emoji,omitempty"` Upload bool `json:"upload,omitempty"` // 过滤参数 Include []string `json:"include,omitempty"` // 包含的关键词 Exclude []string `json:"exclude,omitempty"` // 排除的关键词 Regex string `json:"regex,omitempty"` // 正则表达式过滤 Location string `json:"location,omitempty"` // 地区过滤 RenameRules []string `json:"rename_rules,omitempty"` // 重命名规则 // 高级参数 Test bool `json:"test,omitempty"` // 是否启用测试 List bool `json:"list,omitempty"` // 是否列表模式 Filename string `json:"filename,omitempty"` // 输出文件名 UploadPath string `json:"upload_path,omitempty"` FilterScript string `json:"filter_script,omitempty"` DeviceID string `json:"device_id,omitempty"` Interval string `json:"interval,omitempty"` Groups []string `json:"groups,omitempty"` Rulesets []string `json:"rulesets,omitempty"` BasePath string `json:"-"` Providers []string `json:"providers,omitempty"` EmojiRules []string `json:"emoji_rules,omitempty"` } // NewConversionRequest 创建新的转换请求 // 返回包含默认值的ConversionRequest结构体 func NewConversionRequest() *ConversionRequest { return &ConversionRequest{ Target: "", URL: "", RawURL: "", Sources: []string{}, UserAgent: "", ConfigURL: "", Emoji: false, UDP: false, IPv6: false, Group: "", Insert: false, Strict: false, Compatible: true, // 默认启用兼容模式 Include: []string{}, Exclude: []string{}, Regex: "", Location: "", RenameRules: []string{}, Test: false, List: false, Filename: "", UploadPath: "", FilterScript: "", DeviceID: "", Interval: "", Groups: []string{}, Rulesets: []string{}, BasePath: "", Providers: []string{}, EmojiRules: []string{}, } } // SetURL 设置订阅URL(支持多源) func (r *ConversionRequest) SetURL(raw string) error { trimmed := strings.TrimSpace(raw) r.RawURL = trimmed if trimmed == "" { r.URL = "" r.Sources = nil return nil } sources, err := parseSourcesFromRaw(trimmed) if err != nil { return err } r.Sources = sources if len(sources) > 0 { r.URL = sources[0] } else { r.URL = "" } return nil } // GetSources 返回订阅源列表(拷贝) func (r *ConversionRequest) GetSources() []string { if len(r.Sources) > 0 { out := make([]string, len(r.Sources)) copy(out, r.Sources) return out } if strings.TrimSpace(r.URL) != "" { return []string{strings.TrimSpace(r.URL)} } return nil } // SourceSummary 返回用于记录/展示的源URL字符串 func (r *ConversionRequest) SourceSummary() string { if strings.TrimSpace(r.RawURL) != "" { return r.RawURL } sources := r.GetSources() if len(sources) == 0 { return "" } return strings.Join(sources, "|") } // Validate 验证转换请求的有效性 // 返回error如果请求无效,nil表示请求有效 func (r *ConversionRequest) Validate() error { // 验证必需字段 if r.Target == "" { return fmt.Errorf("target format cannot be empty") } sources := r.GetSources() if len(sources) == 0 && strings.TrimSpace(r.URL) != "" { if err := r.SetURL(r.URL); err != nil { return err } sources = r.GetSources() } if len(sources) == 0 { return fmt.Errorf("source URL cannot be empty") } // 验证目标格式 target := strings.ToLower(r.Target) if target == "auto" { return nil } validTargets := map[string]bool{ "clash": true, "clashr": true, "surge": true, "quanx": true, "loon": true, "surfboard": true, "v2ray": true, } if !validTargets[target] { return fmt.Errorf("unsupported target format: %s, must be one of: clash, clashr, surge, quanx, loon, surfboard, v2ray", r.Target) } // 验证URL格式 for _, src := range sources { if err := r.validateURL(src, "source URL"); err != nil { return err } } // 验证配置URL(如果提供) if r.ConfigURL != "" { if err := r.validateURL(r.ConfigURL, "config URL"); err != nil { return err } } // 验证正则表达式 if r.Regex != "" { if _, err := regexp.Compile(r.Regex); err != nil { return fmt.Errorf("invalid regex pattern: %v", err) } } // 验证文件名 if r.Filename != "" { if len(r.Filename) > 255 { return fmt.Errorf("filename too long (max 255 characters)") } if strings.ContainsAny(r.Filename, "\\/:*?\"<>|") { return fmt.Errorf("filename contains invalid characters") } } return nil } // validateURL 验证URL格式 func (r *ConversionRequest) validateURL(rawURL, fieldName string) error { parsedURL, err := url.Parse(rawURL) if err != nil { return fmt.Errorf("invalid %s: %v", fieldName, err) } if parsedURL.Scheme == "" { return fmt.Errorf("%s must have a scheme (http:// or https://)", fieldName) } if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { return fmt.Errorf("%s scheme must be http or https, got: %s", fieldName, parsedURL.Scheme) } if parsedURL.Host == "" { return fmt.Errorf("%s must have a host", fieldName) } return nil } // FromQuery 从HTTP查询参数解析转换请求 // 根据查询参数自动填充ConversionRequest字段 func (r *ConversionRequest) FromQuery(query string) error { values, err := url.ParseQuery(query) if err != nil { return fmt.Errorf("failed to parse query: %v", err) } splitAndTrim := func(input, sep string) []string { parts := strings.Split(input, sep) out := make([]string, 0, len(parts)) for _, part := range parts { if trimmed := strings.TrimSpace(part); trimmed != "" { out = append(out, trimmed) } } return out } decodeBase64List := func(key, sep string) ([]string, error) { raw := values.Get(key) if raw == "" { return nil, nil } decoded, err := base64.StdEncoding.DecodeString(raw) if err != nil { return nil, fmt.Errorf("invalid %s parameter: %v", key, err) } decodedStr := strings.TrimSpace(string(decoded)) if decodedStr == "" { return []string{}, nil } return splitAndTrim(decodedStr, sep), nil } parseBool := func(key string, setter func(bool)) error { raw := values.Get(key) if raw == "" { return nil } parsed, err := strconv.ParseBool(raw) if err != nil { return fmt.Errorf("invalid %s parameter: %v", key, err) } setter(parsed) return nil } // 基本参数 if target := values.Get("target"); target != "" { r.Target = target } if urlStr := values.Get("url"); urlStr != "" { if err := r.SetURL(urlStr); err != nil { return err } } // 可选参数 if configURL := values.Get("config"); configURL != "" { r.ConfigURL = configURL } if err := parseBool("emoji", func(v bool) { r.Emoji = v }); err != nil { return err } if err := parseBool("udp", func(v bool) { r.UDP = v }); err != nil { return err } if err := parseBool("ipv6", func(v bool) { r.IPv6 = v }); err != nil { return err } if group := values.Get("group"); group != "" { r.Group = group } if err := parseBool("insert", func(v bool) { r.Insert = v }); err != nil { return err } if err := parseBool("strict", func(v bool) { r.Strict = v }); err != nil { return err } if err := parseBool("compatible", func(v bool) { r.Compatible = v }); err != nil { return err } if err := parseBool("append_type", func(v bool) { r.AppendType = v }); err != nil { return err } if err := parseBool("tfo", func(v bool) { r.TFO = v }); err != nil { return err } if err := parseBool("script", func(v bool) { r.Script = v }); err != nil { return err } if err := parseBool("scv", func(v bool) { r.SkipCert = v }); err != nil { return err } if err := parseBool("fdn", func(v bool) { r.FilterDeprecated = v }); err != nil { return err } if err := parseBool("expand", func(v bool) { r.ExpandRules = v }); err != nil { return err } if err := parseBool("append_info", func(v bool) { r.AppendInfo = v }); err != nil { return err } if err := parseBool("prepend", func(v bool) { r.Prepend = v }); err != nil { return err } if err := parseBool("classic", func(v bool) { r.Classic = v }); err != nil { return err } if err := parseBool("tls13", func(v bool) { r.TLS13 = v }); err != nil { return err } if err := parseBool("add_emoji", func(v bool) { r.AddEmoji = v }); err != nil { return err } if err := parseBool("remove_emoji", func(v bool) { r.RemoveEmoji = v }); err != nil { return err } if err := parseBool("upload", func(v bool) { r.Upload = v }); err != nil { return err } // 过滤参数 if include := values.Get("include"); include != "" { r.Include = splitAndTrim(include, ",") } if exclude := values.Get("exclude"); exclude != "" { r.Exclude = splitAndTrim(exclude, ",") } if regex := values.Get("regex"); regex != "" { r.Regex = regex } if location := values.Get("location"); location != "" { r.Location = location } // 高级参数 if err := parseBool("test", func(v bool) { r.Test = v }); err != nil { return err } if err := parseBool("list", func(v bool) { r.List = v }); err != nil { return err } if filename := values.Get("filename"); filename != "" { r.Filename = filename } if uploadPath := values.Get("upload_path"); uploadPath != "" { r.UploadPath = uploadPath } if rename := values.Get("rename"); rename != "" { r.RenameRules = splitAndTrim(rename, "`") } if filterScript := values.Get("filter_script"); filterScript != "" { r.FilterScript = filterScript } if deviceID := values.Get("dev_id"); deviceID != "" { r.DeviceID = deviceID } if interval := values.Get("interval"); interval != "" { r.Interval = interval } if groups, err := decodeBase64List("groups", "@"); err != nil { return err } else if groups != nil { r.Groups = groups } if rulesets, err := decodeBase64List("ruleset", "@"); err != nil { return err } else if rulesets != nil { r.Rulesets = rulesets } if providers, err := decodeBase64List("providers", "@"); err != nil { return err } else if providers != nil { r.Providers = providers } if emojiRules := values.Get("emoji_rule"); emojiRules != "" { r.EmojiRules = splitAndTrim(emojiRules, "`") } if len(r.EmojiRules) == 0 { if emojiRules := values.Get("emoji_rules"); emojiRules != "" { r.EmojiRules = splitAndTrim(emojiRules, "`") } } return r.Validate() } // FromHTTPRequest 从HTTP请求解析转换请求 // 自动从URL查询参数和请求头中提取转换参数 func (r *ConversionRequest) FromHTTPRequest(req *http.Request) error { // 从查询参数解析 if err := r.FromQuery(req.URL.RawQuery); err != nil { return err } // 从请求头中获取额外的信息 if userAgent := req.Header.Get("User-Agent"); userAgent != "" { // 可以根据User-Agent进行特殊处理 } if accept := req.Header.Get("Accept"); accept != "" { // 可以根据Accept头调整输出格式 } return nil } // ToQuery 将转换请求转换为查询字符串 // 返回URL编码的查询字符串 func (r *ConversionRequest) ToQuery() string { values := url.Values{} // 基本参数 values.Set("target", r.Target) values.Set("url", r.SourceSummary()) // 可选参数 if r.ConfigURL != "" { values.Set("config", r.ConfigURL) } values.Set("emoji", strconv.FormatBool(r.Emoji)) values.Set("udp", strconv.FormatBool(r.UDP)) values.Set("ipv6", strconv.FormatBool(r.IPv6)) if r.Group != "" { values.Set("group", r.Group) } values.Set("insert", strconv.FormatBool(r.Insert)) values.Set("strict", strconv.FormatBool(r.Strict)) values.Set("compatible", strconv.FormatBool(r.Compatible)) // 过滤参数 if len(r.Include) > 0 { values.Set("include", strings.Join(r.Include, ",")) } if len(r.Exclude) > 0 { values.Set("exclude", strings.Join(r.Exclude, ",")) } if r.Regex != "" { values.Set("regex", r.Regex) } if r.Location != "" { values.Set("location", r.Location) } // 高级参数 values.Set("test", strconv.FormatBool(r.Test)) values.Set("list", strconv.FormatBool(r.List)) if r.Filename != "" { values.Set("filename", r.Filename) } return values.Encode() } // Clone 创建转换请求的副本 // 返回新的ConversionRequest实例 func (r *ConversionRequest) Clone() *ConversionRequest { return &ConversionRequest{ Target: r.Target, URL: r.URL, RawURL: r.RawURL, Sources: append([]string{}, r.Sources...), ConfigURL: r.ConfigURL, Emoji: r.Emoji, UDP: r.UDP, Group: r.Group, Insert: r.Insert, Strict: r.Strict, Compatible: r.Compatible, Include: append([]string{}, r.Include...), Exclude: append([]string{}, r.Exclude...), Regex: r.Regex, Location: r.Location, Test: r.Test, List: r.List, Filename: r.Filename, Providers: append([]string{}, r.Providers...), EmojiRules: append([]string{}, r.EmojiRules...), } } // CacheSignature returns a canonical representation of the request for caching. func (r *ConversionRequest) CacheSignature() string { sources := r.GetSources() signature := struct { Target string `json:"target"` Sources []string `json:"sources"` ConfigURL string `json:"config_url"` Emoji bool `json:"emoji"` UDP bool `json:"udp"` IPv6 bool `json:"ipv6"` Group string `json:"group"` Insert bool `json:"insert"` Strict bool `json:"strict"` Compatible bool `json:"compatible"` AppendType bool `json:"append_type"` TFO bool `json:"tfo"` Script bool `json:"script"` SkipCert bool `json:"skip_cert"` FilterDeprecated bool `json:"filter_deprecated"` ExpandRules bool `json:"expand_rules"` AppendInfo bool `json:"append_info"` Prepend bool `json:"prepend"` Classic bool `json:"classic"` TLS13 bool `json:"tls13"` AddEmoji bool `json:"add_emoji"` RemoveEmoji bool `json:"remove_emoji"` Upload bool `json:"upload"` UploadPath string `json:"upload_path"` Filename string `json:"filename"` Test bool `json:"test"` List bool `json:"list"` DeviceID string `json:"device_id"` Interval string `json:"interval"` Include []string `json:"include"` Exclude []string `json:"exclude"` Regex string `json:"regex"` Location string `json:"location"` RenameRules []string `json:"rename_rules"` Groups []string `json:"groups"` Rulesets []string `json:"rulesets"` Providers []string `json:"providers"` EmojiRules []string `json:"emoji_rules"` FilterScript string `json:"filter_script"` BasePath string `json:"base_path"` }{ Target: strings.ToLower(r.Target), Sources: append([]string{}, sources...), ConfigURL: r.ConfigURL, Emoji: r.Emoji, UDP: r.UDP, IPv6: r.IPv6, Group: r.Group, Insert: r.Insert, Strict: r.Strict, Compatible: r.Compatible, AppendType: r.AppendType, TFO: r.TFO, Script: r.Script, SkipCert: r.SkipCert, FilterDeprecated: r.FilterDeprecated, ExpandRules: r.ExpandRules, AppendInfo: r.AppendInfo, Prepend: r.Prepend, Classic: r.Classic, TLS13: r.TLS13, AddEmoji: r.AddEmoji, RemoveEmoji: r.RemoveEmoji, Upload: r.Upload, UploadPath: r.UploadPath, Filename: r.Filename, Test: r.Test, List: r.List, DeviceID: r.DeviceID, Interval: r.Interval, Include: append([]string{}, r.Include...), Exclude: append([]string{}, r.Exclude...), Regex: r.Regex, Location: r.Location, RenameRules: append([]string{}, r.RenameRules...), Groups: append([]string{}, r.Groups...), Rulesets: append([]string{}, r.Rulesets...), Providers: append([]string{}, r.Providers...), EmojiRules: append([]string{}, r.EmojiRules...), FilterScript: r.FilterScript, BasePath: r.BasePath, } data, err := json.Marshal(signature) if err != nil { return fmt.Sprintf("%s|%s|%t|%t|%t", signature.Target, r.SourceSummary(), r.AddEmoji, r.RemoveEmoji, r.Upload) } return string(data) } func parseSourcesFromRaw(raw string) ([]string, error) { trimmed := strings.TrimSpace(raw) if trimmed == "" { return nil, nil } sources := splitSources(trimmed) if len(sources) <= 1 { if decoded, ok := decodeURLListFromBase64(trimmed); ok { sources = splitSources(decoded) } } if len(sources) == 0 && trimmed != "" { sources = []string{trimmed} } return sources, nil } func splitSources(input string) []string { if input == "" { return nil } normalized := strings.ReplaceAll(input, "\r\n", "\n") normalized = strings.ReplaceAll(normalized, "\r", "\n") parts := strings.FieldsFunc(normalized, func(r rune) bool { switch r { case '|', '\n', ',', ';': return true default: return false } }) result := make([]string, 0, len(parts)) for _, part := range parts { if trimmed := strings.TrimSpace(part); trimmed != "" { result = append(result, trimmed) } } if len(result) == 0 { if trimmed := strings.TrimSpace(input); trimmed != "" { result = append(result, trimmed) } } return result } func decodeURLListFromBase64(raw string) (string, bool) { data, ok := decodeBase64String(raw) if !ok || len(data) == 0 { return "", false } decoded := strings.TrimSpace(string(data)) lower := strings.ToLower(decoded) if !strings.Contains(lower, "http://") && !strings.Contains(lower, "https://") { return "", false } return decoded, true } func decodeBase64String(raw string) ([]byte, bool) { s := strings.TrimSpace(raw) if s == "" { return nil, false } s = strings.ReplaceAll(s, "\n", "") s = strings.ReplaceAll(s, "\r", "") if rem := len(s) % 4; rem != 0 { s += strings.Repeat("=", 4-rem) } for _, enc := range []*base64.Encoding{base64.StdEncoding, base64.URLEncoding} { decoded, err := enc.DecodeString(s) if err == nil && len(decoded) > 0 { return decoded, true } } return nil, false } // MatchesFilter 检查代理是否匹配过滤条件 // 返回true如果代理匹配所有过滤条件 func (r *ConversionRequest) MatchesFilter(proxyName, proxyLocation string) bool { // 检查包含关键词 for _, include := range r.Include { if !strings.Contains(strings.ToLower(proxyName), strings.ToLower(include)) { return false } } // 检查排除关键词 for _, exclude := range r.Exclude { if strings.Contains(strings.ToLower(proxyName), strings.ToLower(exclude)) { return false } } // 检查正则表达式 if r.Regex != "" { if matched, err := regexp.MatchString(r.Regex, proxyName); err != nil || !matched { return false } } // 检查地区 if r.Location != "" && proxyLocation != "" { if !strings.EqualFold(proxyLocation, r.Location) { return false } } return true } // GetOutputFilename 获取输出文件名 // 根据配置生成合适的输出文件名 func (r *ConversionRequest) GetOutputFilename() string { if r.Filename != "" { // 确保有正确的扩展名 if !strings.HasSuffix(r.Filename, "."+r.GetFileExtension()) { return r.Filename + "." + r.GetFileExtension() } return r.Filename } // 生成默认文件名 defaultName := "converted" if r.Group != "" { defaultName = r.Group } return fmt.Sprintf("%s.%s", defaultName, r.GetFileExtension()) } // GetFileExtension 获取目标格式的文件扩展名 // 返回对应格式的文件扩展名 func (r *ConversionRequest) GetFileExtension() string { switch strings.ToLower(r.Target) { case "clash": return "yaml" case "surge": return "conf" case "quanx": return "conf" case "loon": return "conf" case "surfboard": return "conf" case "v2ray": return "json" default: return "txt" } } // GetContentType 获取响应的Content-Type // 返回适合目标格式的MIME类型 func (r *ConversionRequest) GetContentType() string { if r.List { return "text/plain; charset=utf-8" } switch strings.ToLower(r.Target) { case "clash": return "text/yaml; charset=utf-8" case "surge", "quanx", "loon", "surfboard": return "text/plain; charset=utf-8" case "v2ray": return "application/json; charset=utf-8" default: return "text/plain; charset=utf-8" } } // IsCompatible 检查是否与C++版本兼容 // 返回true如果使用兼容模式 func (r *ConversionRequest) IsCompatible() bool { return r.Compatible } // ShouldIncludeTests 检查是否应该包含测试信息 // 返回true如果启用了测试模式 func (r *ConversionRequest) ShouldIncludeTests() bool { return r.Test } // ShouldShowList 检查是否应该显示列表 // 返回true如果是列表模式 func (r *ConversionRequest) ShouldShowList() bool { return r.List } // ShouldInsertURLTest 检查是否应该插入URL测试 // 返回true如果启用了插入URL测试 func (r *ConversionRequest) ShouldInsertURLTest() bool { return r.Insert } // IsStrictMode 检查是否是严格模式 // 返回true如果启用了严格模式 func (r *ConversionRequest) IsStrictMode() bool { return r.Strict }