package app import ( "context" "crypto/x509" "errors" "fmt" "io" "net" "net/http" "regexp" "sort" "strings" "syscall" "time" "gopkg.in/yaml.v3" ) const maxDownload = 8 << 20 type subscriptionDocument struct { Proxies []map[string]any `yaml:"proxies"` Providers map[string]struct { Type string `yaml:"type"` URL string `yaml:"url"` Payload []map[string]any `yaml:"payload"` Filter string `yaml:"filter"` Exclude string `yaml:"exclude-filter"` Override map[string]any `yaml:"override"` Header map[string][]string `yaml:"header"` } `yaml:"proxy-providers"` } func fetchSubscription(ctx context.Context, client *http.Client, raw string, headers map[string][]string) ([]byte, string, error) { if err := validHTTPURL(raw); err != nil { return nil, "", err } req, err := http.NewRequestWithContext(ctx, "GET", raw, nil) if err != nil { return nil, "", errors.New("无效的订阅地址") } req.Header.Set("User-Agent", "clash.meta/ClashWeb") for k, values := range headers { for _, v := range values { req.Header.Add(k, v) } } resp, err := client.Do(req) if err != nil { return nil, "", subscriptionDownloadError(err) } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, "", fmt.Errorf("订阅下载失败(HTTP %d)", resp.StatusCode) } b, err := io.ReadAll(io.LimitReader(resp.Body, maxDownload+1)) if err != nil { return nil, "", errors.New("订阅读取失败") } if len(b) > maxDownload { return nil, "", errors.New("订阅文件超过 8 MiB 限制") } return b, resp.Header.Get("Subscription-Userinfo"), nil } func subscriptionDownloadError(err error) error { // Do not return url.Error.String(): it includes the private subscription URL. var authority x509.UnknownAuthorityError var hostname x509.HostnameError var certificate x509.CertificateInvalidError var dns *net.DNSError var network net.Error switch { case errors.As(err, &authority): return errors.New("订阅 TLS 证书不受信任,请检查 VM 的 CA 证书包;若站点使用自签名证书,需修复其信任配置") case errors.As(err, &hostname): return errors.New("订阅 TLS 证书与域名不匹配,请核对订阅地址") case errors.As(err, &certificate): return errors.New("订阅 TLS 证书校验失败,请检查 VM 时间和站点证书有效期") case errors.As(err, &dns): return errors.New("订阅域名解析失败,请检查 VM 的系统 DNS 配置") case errors.Is(err, context.Canceled): return errors.New("订阅下载已取消") case errors.Is(err, context.DeadlineExceeded), errors.As(err, &network) && network.Timeout(): return errors.New("订阅下载超时,请检查 VM 到订阅服务器的网络或下载代理") case errors.Is(err, syscall.ECONNREFUSED): return errors.New("订阅连接被拒绝,请检查服务器或下载代理端口") default: return errors.New("订阅连接失败,请检查 VM 的网络、TLS 和下载代理配置") } } func parseDocument(data []byte) (subscriptionDocument, error) { var doc subscriptionDocument if err := yaml.Unmarshal(data, &doc); err != nil { return doc, errors.New("订阅不是有效的 Clash/Mihomo YAML;不支持 Base64 订阅") } if len(doc.Proxies) == 0 && len(doc.Providers) == 0 { return doc, errors.New("订阅中未发现 proxies 或 proxy-providers") } return doc, nil } func loadSubscription(ctx context.Context, sub Subscription, client *http.Client) ([]map[string]any, string, error) { ctx, cancel := context.WithTimeout(ctx, 90*time.Second) defer cancel() b, usage, err := fetchSubscription(ctx, client, sub.URL, nil) if err != nil { return nil, "", err } doc, err := parseDocument(b) if err != nil { return nil, "", err } type batch struct { source string nodes []map[string]any } batches := []batch{{"inline", doc.Proxies}} keys := []string{} for key := range doc.Providers { keys = append(keys, key) } sort.Strings(keys) if len(keys) > 20 { return nil, "", errors.New("单个订阅最多允许 20 个节点提供者") } for _, key := range keys { provider := doc.Providers[key] nodes := provider.Payload if provider.Type == "http" { data, _, err := fetchSubscription(ctx, client, provider.URL, provider.Header) if err != nil { return nil, "", fmt.Errorf("节点提供者下载失败: %w", err) } var p subscriptionDocument if err := yaml.Unmarshal(data, &p); err != nil { return nil, "", errors.New("节点提供者返回了无效 YAML") } nodes = p.Proxies } else if provider.Type != "inline" { return nil, "", errors.New("仅支持 HTTP 和 inline 节点提供者,不读取订阅指定的本地文件") } var filter, exclude *regexp.Regexp if provider.Filter != "" { filter, err = regexp.Compile(provider.Filter) if err != nil { return nil, "", errors.New("节点提供者筛选表达式无效") } } if provider.Exclude != "" { exclude, err = regexp.Compile(provider.Exclude) if err != nil { return nil, "", errors.New("节点提供者排除表达式无效") } } filtered := []map[string]any{} for _, node := range nodes { name, _ := node["name"].(string) if filter != nil && !filter.MatchString(name) { continue } if exclude != nil && exclude.MatchString(name) { continue } for k, v := range provider.Override { switch k { case "additional-prefix", "additional-suffix": default: node[k] = v } } prefix, _ := provider.Override["additional-prefix"].(string) suffix, _ := provider.Override["additional-suffix"].(string) node["name"] = prefix + name + suffix filtered = append(filtered, node) } batches = append(batches, batch{key, filtered}) } result := []map[string]any{} for _, batch := range batches { renames := map[string]string{} for _, node := range batch.nodes { name, ok := node["name"].(string) kind, _ := node["type"].(string) if !ok || strings.TrimSpace(name) == "" || kind == "" || len(name) > 256 { return nil, "", errors.New("节点缺少有效名称或协议类型") } if _, exists := renames[name]; exists { return nil, "", errors.New("同一个节点提供者中存在同名节点") } renames[name] = sub.ID + "/" + batch.source + "/" + name } for _, node := range batch.nodes { if dialer, ok := node["dialer-proxy"].(string); ok && dialer != "" { if renamed, ok := renames[dialer]; ok { node["dialer-proxy"] = renamed } else { return nil, "", errors.New("节点链式代理引用了无法导入的策略组或其他节点提供者") } } name := node["name"].(string) node["name"] = renames[name] result = append(result, node) } } if len(result) == 0 { return nil, "", errors.New("订阅中没有可用的节点") } if len(result) > 5000 { return nil, "", errors.New("单个订阅最多允许 5000 个节点") } return result, usage, nil }