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,825 @@
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
}