Files

216 lines
6.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}