mirror of
https://github.com/bestnite/sub2clash.git
synced 2026-04-26 12:51:52 +00:00
683 lines
20 KiB
Go
683 lines
20 KiB
Go
package common
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"io"
|
||
"net/url"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/bestnite/sub2clash/logger"
|
||
"github.com/bestnite/sub2clash/model"
|
||
P "github.com/bestnite/sub2clash/model/proxy"
|
||
"github.com/bestnite/sub2clash/parser"
|
||
"github.com/bestnite/sub2clash/utils"
|
||
"go.uber.org/zap"
|
||
"gopkg.in/yaml.v3"
|
||
)
|
||
|
||
var subsDir = "subs"
|
||
var fileLock sync.RWMutex
|
||
|
||
func LoadSubscription(url string, refresh bool, userAgent string, cacheExpire int64, retryTimes int) ([]byte, error) {
|
||
if refresh {
|
||
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
|
||
}
|
||
hash := sha256.Sum224([]byte(url))
|
||
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
|
||
stat, err := os.Stat(fileName)
|
||
if err != nil {
|
||
if !os.IsNotExist(err) {
|
||
return nil, err
|
||
}
|
||
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
|
||
}
|
||
lastGetTime := stat.ModTime().Unix()
|
||
if lastGetTime+cacheExpire > time.Now().Unix() {
|
||
file, err := os.Open(fileName)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer func(file *os.File) {
|
||
if file != nil {
|
||
_ = file.Close()
|
||
}
|
||
}(file)
|
||
fileLock.RLock()
|
||
defer fileLock.RUnlock()
|
||
subContent, err := io.ReadAll(file)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return subContent, nil
|
||
}
|
||
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
|
||
}
|
||
|
||
func FetchSubscriptionFromAPI(url string, userAgent string, retryTimes int) ([]byte, error) {
|
||
hash := sha256.Sum224([]byte(url))
|
||
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
|
||
client := Request(retryTimes)
|
||
defer client.Close()
|
||
resp, err := client.R().SetHeader("User-Agent", userAgent).Get(url)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
data, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||
}
|
||
file, err := os.Create(fileName)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer func(file *os.File) {
|
||
if file != nil {
|
||
_ = file.Close()
|
||
}
|
||
}(file)
|
||
fileLock.Lock()
|
||
defer fileLock.Unlock()
|
||
_, err = file.Write(data)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to write to sub.yaml: %w", err)
|
||
}
|
||
return data, nil
|
||
}
|
||
|
||
// BuildSub 是当前配置转换链路的核心入口。
|
||
//
|
||
// 当前设计分为三层:
|
||
// 1. templateDoc:模板 YAML 的完整语法树,也是最终输出真源
|
||
// 2. generatedConfig:本项目运行期最小叠加层,只保存参与业务计算的字段
|
||
// 3. proxy.Proxy:节点解析后的 typed 模型,用于过滤、去重、重命名和输出
|
||
//
|
||
// 这个函数的目标不是“重建一整份 mihomo 配置”,而是:
|
||
// - 保留模板中绝大部分原始字段
|
||
// - 只对 proxies / proxy-groups / rules / rule-providers 做定点 patch
|
||
func BuildSub(clashType model.ClashType, query model.ConvertConfig, template string, cacheExpire int64, retryTimes int) (
|
||
*BuiltSub, error,
|
||
) {
|
||
templateDoc, templateBytes, err := loadTemplateDocument(query, template, cacheExpire, retryTimes)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
temp, err := extractTemplateOverlay(templateDoc)
|
||
if err != nil {
|
||
logger.Logger.Debug("extract template overlay failed", zap.Error(err))
|
||
return nil, NewTemplateParseError(templateBytes, err)
|
||
}
|
||
proxyList, err := collectQueryProxies(query, cacheExpire, retryTimes)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
proxyList, err = normalizeProxyList(query, proxyList)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// t 仅承载“由节点生成出来的新内容”,例如国家组。
|
||
// 模板里原有的组、规则等则保存在 temp 中。
|
||
generated, err := buildGeneratedConfig(clashType, query, proxyList)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
MergeSubAndTemplate(temp, generated, query.IgnoreCountryGrooup)
|
||
|
||
applyRulePatches(temp, query)
|
||
addedRuleProviders := buildRuleProviderPatches(query)
|
||
|
||
if err := mergeTemplateProxies(templateDoc, generated.Proxy); err != nil {
|
||
return nil, NewError(ErrConfigInvalid, "failed to update template path: proxies", err)
|
||
}
|
||
|
||
if temp.ProxyGroup == nil {
|
||
temp.ProxyGroup = make([]generatedGroup, 0)
|
||
}
|
||
if err := mergeTemplateProxyGroups(templateDoc, temp.ProxyGroup); err != nil {
|
||
return nil, NewError(ErrConfigInvalid, "failed to update template path: proxy-groups", err)
|
||
}
|
||
|
||
rulesChanged := len(query.Rules) != 0 || len(query.RuleProviders) != 0
|
||
if rulesChanged {
|
||
if temp.Rule == nil {
|
||
temp.Rule = make([]string, 0)
|
||
}
|
||
if err := SetYAMLPath(templateDoc, "rules", temp.Rule); err != nil {
|
||
return nil, NewError(ErrConfigInvalid, "failed to update template path: rules", err)
|
||
}
|
||
}
|
||
|
||
if len(query.RuleProviders) != 0 {
|
||
if err := mergeTemplateRuleProviders(templateDoc, addedRuleProviders); err != nil {
|
||
return nil, NewError(ErrConfigInvalid, "failed to update template path: rule-providers", err)
|
||
}
|
||
}
|
||
|
||
return &BuiltSub{root: templateDoc}, nil
|
||
}
|
||
|
||
// loadTemplateDocument 负责统一加载模板来源,并返回:
|
||
// 1. 解析后的 YAML 语法树
|
||
// 2. 原始模板字节,用于错误报告
|
||
func loadTemplateDocument(query model.ConvertConfig, template string, cacheExpire int64, retryTimes int) (*yaml.Node, []byte, error) {
|
||
var err error
|
||
var templateBytes []byte
|
||
|
||
if query.Template != "" {
|
||
template = query.Template
|
||
}
|
||
if strings.HasPrefix(template, "http") {
|
||
templateBytes, err = LoadSubscription(template, query.Refresh, query.UserAgent, cacheExpire, retryTimes)
|
||
if err != nil {
|
||
logger.Logger.Debug(
|
||
"load template failed", zap.String("template", template), zap.Error(err),
|
||
)
|
||
return nil, nil, NewTemplateLoadError(template, err)
|
||
}
|
||
} else {
|
||
unescape, err := url.QueryUnescape(template)
|
||
if err != nil {
|
||
return nil, nil, NewTemplateLoadError(template, err)
|
||
}
|
||
templateBytes, err = LoadTemplate(unescape)
|
||
if err != nil {
|
||
logger.Logger.Debug(
|
||
"load template failed", zap.String("template", template), zap.Error(err),
|
||
)
|
||
return nil, nil, NewTemplateLoadError(unescape, err)
|
||
}
|
||
}
|
||
|
||
templateDoc, err := ParseYAMLDocument(templateBytes)
|
||
if err != nil {
|
||
logger.Logger.Debug("parse template yaml node failed", zap.Error(err))
|
||
return nil, templateBytes, NewTemplateParseError(templateBytes, err)
|
||
}
|
||
|
||
return templateDoc, templateBytes, nil
|
||
}
|
||
|
||
// collectQueryProxies 汇总来自订阅链接和直接传入代理链接的所有节点。
|
||
func collectQueryProxies(query model.ConvertConfig, cacheExpire int64, retryTimes int) ([]P.Proxy, error) {
|
||
proxyList := make([]P.Proxy, 0)
|
||
for i := range query.Subs {
|
||
newProxies, err := loadSubscriptionProxies(query, query.Subs[i], cacheExpire, retryTimes)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
proxyList = append(proxyList, newProxies...)
|
||
}
|
||
|
||
if len(query.Proxies) != 0 {
|
||
p, err := parser.ParseProxies(parser.ParseConfig{UseUDP: query.UseUDP}, query.Proxies...)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
proxyList = append(proxyList, p...)
|
||
}
|
||
|
||
return proxyList, nil
|
||
}
|
||
|
||
// loadSubscriptionProxies 负责加载单条订阅并应用订阅名作为节点前缀。
|
||
func loadSubscriptionProxies(query model.ConvertConfig, subscriptionURL string, cacheExpire int64, retryTimes int) ([]P.Proxy, error) {
|
||
data, err := LoadSubscription(subscriptionURL, query.Refresh, query.UserAgent, cacheExpire, retryTimes)
|
||
if err != nil {
|
||
logger.Logger.Debug(
|
||
"load subscription failed", zap.String("url", subscriptionURL), zap.Error(err),
|
||
)
|
||
return nil, NewSubscriptionLoadError(subscriptionURL, err)
|
||
}
|
||
|
||
subName := ""
|
||
if strings.Contains(subscriptionURL, "#") {
|
||
subName = subscriptionURL[strings.LastIndex(subscriptionURL, "#")+1:]
|
||
}
|
||
|
||
newProxies, err := parseSubscriptionProxies(data, query.UseUDP, subscriptionURL)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if subName != "" {
|
||
for i := range newProxies {
|
||
newProxies[i].SubName = subName
|
||
}
|
||
}
|
||
|
||
return newProxies, nil
|
||
}
|
||
|
||
// parseSubscriptionProxies 按“Clash YAML -> URI 列表 -> Base64 文本”的顺序容错解析节点。
|
||
func parseSubscriptionProxies(data []byte, useUDP bool, subscriptionURL string) ([]P.Proxy, error) {
|
||
sub := &proxyListDoc{}
|
||
if err := yaml.Unmarshal(data, sub); err == nil {
|
||
return sub.Proxy, nil
|
||
}
|
||
|
||
reg, err := regexp.Compile("(" + strings.Join(parser.GetAllPrefixes(), "|") + ")://")
|
||
if err != nil {
|
||
logger.Logger.Debug("compile regex failed", zap.Error(err))
|
||
return nil, NewRegexInvalidError("prefix", err)
|
||
}
|
||
|
||
if reg.Match(data) {
|
||
return parser.ParseProxies(parser.ParseConfig{UseUDP: useUDP}, strings.Split(string(data), "\n")...)
|
||
}
|
||
|
||
base64, err := utils.DecodeBase64(string(data), false)
|
||
if err != nil {
|
||
logger.Logger.Debug(
|
||
"parse subscription failed", zap.String("url", subscriptionURL),
|
||
zap.String("data", string(data)),
|
||
zap.Error(err),
|
||
)
|
||
return nil, NewSubscriptionParseError(data, err)
|
||
}
|
||
|
||
return parser.ParseProxies(parser.ParseConfig{UseUDP: useUDP}, strings.Split(base64, "\n")...)
|
||
}
|
||
|
||
// normalizeProxyList 汇总所有节点标准化步骤,确保后续分组和 patch 使用的是稳定结果。
|
||
func normalizeProxyList(query model.ConvertConfig, proxyList []P.Proxy) ([]P.Proxy, error) {
|
||
applySubscriptionPrefixes(proxyList)
|
||
|
||
var err error
|
||
proxyList, err = dedupeProxies(proxyList)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
proxyList, err = removeProxiesByPattern(proxyList, query.Remove)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
proxyList, err = replaceProxyNames(proxyList, query.Replace)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
ensureUniqueProxyNames(proxyList)
|
||
trimProxyNames(proxyList)
|
||
return proxyList, nil
|
||
}
|
||
|
||
func applySubscriptionPrefixes(proxyList []P.Proxy) {
|
||
for i := range proxyList {
|
||
if proxyList[i].SubName != "" {
|
||
proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name)
|
||
}
|
||
}
|
||
}
|
||
|
||
// dedupeProxies 通过 YAML 序列化结果判定两个节点是否完全相同。
|
||
func dedupeProxies(proxyList []P.Proxy) ([]P.Proxy, error) {
|
||
proxies := make(map[string]*P.Proxy)
|
||
newProxies := make([]P.Proxy, 0, len(proxyList))
|
||
for i := range proxyList {
|
||
yamlBytes, err := yaml.Marshal(proxyList[i])
|
||
if err != nil {
|
||
logger.Logger.Debug("marshal proxy failed", zap.Error(err))
|
||
return nil, fmt.Errorf("marshal proxy failed: %w", err)
|
||
}
|
||
key := string(yamlBytes)
|
||
if _, exist := proxies[key]; !exist {
|
||
proxies[key] = &proxyList[i]
|
||
newProxies = append(newProxies, proxyList[i])
|
||
}
|
||
}
|
||
return newProxies, nil
|
||
}
|
||
|
||
func removeProxiesByPattern(proxyList []P.Proxy, pattern string) ([]P.Proxy, error) {
|
||
if strings.TrimSpace(pattern) == "" {
|
||
return proxyList, nil
|
||
}
|
||
|
||
removeReg, err := regexp.Compile(pattern)
|
||
if err != nil {
|
||
logger.Logger.Debug("remove regexp compile failed", zap.Error(err))
|
||
return nil, NewRegexInvalidError("remove", err)
|
||
}
|
||
|
||
newProxyList := make([]P.Proxy, 0, len(proxyList))
|
||
for i := range proxyList {
|
||
if removeReg.MatchString(proxyList[i].Name) {
|
||
continue
|
||
}
|
||
newProxyList = append(newProxyList, proxyList[i])
|
||
}
|
||
return newProxyList, nil
|
||
}
|
||
|
||
func replaceProxyNames(proxyList []P.Proxy, replacements map[string]string) ([]P.Proxy, error) {
|
||
if len(replacements) == 0 {
|
||
return proxyList, nil
|
||
}
|
||
|
||
for pattern, replacement := range replacements {
|
||
replaceReg, err := regexp.Compile(pattern)
|
||
if err != nil {
|
||
logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
|
||
return nil, NewRegexInvalidError("replace", err)
|
||
}
|
||
for i := range proxyList {
|
||
if replaceReg.MatchString(proxyList[i].Name) {
|
||
proxyList[i].Name = replaceReg.ReplaceAllString(proxyList[i].Name, replacement)
|
||
}
|
||
}
|
||
}
|
||
|
||
return proxyList, nil
|
||
}
|
||
|
||
func ensureUniqueProxyNames(proxyList []P.Proxy) {
|
||
names := make(map[string]int)
|
||
for i := range proxyList {
|
||
if _, exist := names[proxyList[i].Name]; exist {
|
||
names[proxyList[i].Name] = names[proxyList[i].Name] + 1
|
||
proxyList[i].Name = proxyList[i].Name + " " + strconv.Itoa(names[proxyList[i].Name])
|
||
} else {
|
||
names[proxyList[i].Name] = 0
|
||
}
|
||
}
|
||
}
|
||
|
||
func trimProxyNames(proxyList []P.Proxy) {
|
||
for i := range proxyList {
|
||
proxyList[i].Name = strings.TrimSpace(proxyList[i].Name)
|
||
}
|
||
}
|
||
|
||
// buildGeneratedConfig 只生成“新增内容”,例如国家组和最终可输出的节点集合。
|
||
func buildGeneratedConfig(clashType model.ClashType, query model.ConvertConfig, proxyList []P.Proxy) (*generatedConfig, error) {
|
||
generated := &generatedConfig{}
|
||
AddProxy(generated, query.AutoTest, query.Lazy, clashType, proxyList...)
|
||
sortGeneratedGroups(generated, query.Sort)
|
||
return generated, nil
|
||
}
|
||
|
||
func sortGeneratedGroups(generated *generatedConfig, sortMode string) {
|
||
switch sortMode {
|
||
case "sizeasc":
|
||
sort.Sort(generatedGroupsSortBySize(generated.ProxyGroup))
|
||
case "sizedesc":
|
||
sort.Sort(sort.Reverse(generatedGroupsSortBySize(generated.ProxyGroup)))
|
||
case "nameasc":
|
||
sort.Sort(generatedGroupsSortByName(generated.ProxyGroup))
|
||
case "namedesc":
|
||
sort.Sort(sort.Reverse(generatedGroupsSortByName(generated.ProxyGroup)))
|
||
default:
|
||
sort.Sort(generatedGroupsSortByName(generated.ProxyGroup))
|
||
}
|
||
}
|
||
|
||
// applyRulePatches 只修改运行期 overlay 中的 rules 切片,不直接写 YAML。
|
||
func applyRulePatches(temp *generatedConfig, query model.ConvertConfig) {
|
||
for _, v := range query.Rules {
|
||
if v.Prepend {
|
||
PrependRules(temp, v.Rule)
|
||
} else {
|
||
AppendRules(temp, v.Rule)
|
||
}
|
||
}
|
||
for _, v := range query.RuleProviders {
|
||
if v.Prepend {
|
||
PrependRuleProvider(temp, v.Name, v.Group)
|
||
} else {
|
||
AppenddRuleProvider(temp, v.Name, v.Group)
|
||
}
|
||
}
|
||
}
|
||
|
||
// buildRuleProviderPatches 把 API 请求中的 rule-provider 参数转换成 YAML patch payload。
|
||
func buildRuleProviderPatches(query model.ConvertConfig) map[string]generatedRulePatch {
|
||
if len(query.RuleProviders) == 0 {
|
||
return nil
|
||
}
|
||
|
||
patches := make(map[string]generatedRulePatch, len(query.RuleProviders))
|
||
for _, v := range query.RuleProviders {
|
||
hash := sha256.Sum224([]byte(v.Url))
|
||
name := hex.EncodeToString(hash[:])
|
||
patches[v.Name] = generatedRulePatch{
|
||
Type: "http",
|
||
Behavior: v.Behavior,
|
||
Url: v.Url,
|
||
Path: "./" + name + ".yaml",
|
||
Interval: 3600,
|
||
}
|
||
}
|
||
return patches
|
||
}
|
||
|
||
// extractTemplateOverlay 只从模板 YAML 树中提取本项目真正会参与计算的局部字段。
|
||
// 这让模板读取完全基于 yaml.Node,而不再依赖任何整份配置的 typed unmarshal。
|
||
func extractTemplateOverlay(templateDoc *yaml.Node) (*generatedConfig, error) {
|
||
overlay := &generatedConfig{}
|
||
|
||
if err := decodeOptionalYAMLPath(templateDoc, "proxy-groups", &overlay.ProxyGroup); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := decodeOptionalYAMLPath(templateDoc, "rules", &overlay.Rule); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return overlay, nil
|
||
}
|
||
|
||
// decodeOptionalYAMLPath 在路径存在且非 null 时才执行 Decode,
|
||
// 路径不存在时保持目标值为零值。
|
||
func decodeOptionalYAMLPath(doc *yaml.Node, path string, target any) error {
|
||
node, err := GetYAMLPath(doc, path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if node == nil || isNullYAMLNode(node) {
|
||
return nil
|
||
}
|
||
if err := node.Decode(target); err != nil {
|
||
return fmt.Errorf("decode template path %q failed: %w", path, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// mergeTemplateProxies 只负责把本项目生成出的代理追加到模板现有 proxies 后面。
|
||
// 模板中已有代理节点原样保留,不做 struct round-trip。
|
||
func mergeTemplateProxies(templateDoc *yaml.Node, generated []P.Proxy) error {
|
||
if len(generated) == 0 && !HasYAMLPath(templateDoc, "proxies") {
|
||
return nil
|
||
}
|
||
|
||
proxiesNode, err := EnsureYAMLSequencePath(templateDoc, "proxies")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
for _, proxy := range generated {
|
||
if err := AppendYAMLSequenceValue(proxiesNode, proxy); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// mergeTemplateProxyGroups 负责两类更新:
|
||
// 1. 对模板中同名组,仅覆盖 proxies 字段,保留其他字段
|
||
// 2. 追加本项目新生成的国家组
|
||
func mergeTemplateProxyGroups(templateDoc *yaml.Node, groups []generatedGroup) error {
|
||
if len(groups) == 0 && !HasYAMLPath(templateDoc, "proxy-groups") {
|
||
return nil
|
||
}
|
||
|
||
groupNodes, err := EnsureYAMLSequencePath(templateDoc, "proxy-groups")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
for _, group := range groups {
|
||
if group.IsCountry {
|
||
if existing := FindYAMLSequenceMappingByStringField(groupNodes, "name", group.Name); existing != nil {
|
||
continue
|
||
}
|
||
if err := AppendYAMLSequenceValue(groupNodes, group); err != nil {
|
||
return err
|
||
}
|
||
continue
|
||
}
|
||
|
||
existing := FindYAMLSequenceMappingByStringField(groupNodes, "name", group.Name)
|
||
if existing == nil {
|
||
if err := AppendYAMLSequenceValue(groupNodes, group); err != nil {
|
||
return err
|
||
}
|
||
continue
|
||
}
|
||
|
||
if findMappingValue(existing, "proxies") == nil {
|
||
continue
|
||
}
|
||
|
||
if err := SetYAMLMappingField(existing, "proxies", group.Proxies); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// mergeTemplateRuleProviders 以字段级 patch 的方式更新/插入 rule-provider,
|
||
// 以避免覆盖模板中已有 provider 的未知字段。
|
||
func mergeTemplateRuleProviders(templateDoc *yaml.Node, providers map[string]generatedRulePatch) error {
|
||
if len(providers) == 0 && !HasYAMLPath(templateDoc, "rule-providers") {
|
||
return nil
|
||
}
|
||
|
||
providerNodes, err := EnsureYAMLMappingPath(templateDoc, "rule-providers")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
for name, provider := range providers {
|
||
existing := findMappingValue(providerNodes, name)
|
||
if existing != nil && existing.Kind == yaml.MappingNode {
|
||
if err := SetYAMLMappingField(existing, "type", provider.Type); err != nil {
|
||
return err
|
||
}
|
||
if err := SetYAMLMappingField(existing, "behavior", provider.Behavior); err != nil {
|
||
return err
|
||
}
|
||
if err := SetYAMLMappingField(existing, "url", provider.Url); err != nil {
|
||
return err
|
||
}
|
||
if err := SetYAMLMappingField(existing, "path", provider.Path); err != nil {
|
||
return err
|
||
}
|
||
if err := SetYAMLMappingField(existing, "interval", provider.Interval); err != nil {
|
||
return err
|
||
}
|
||
if provider.Format != "" {
|
||
if err := SetYAMLMappingField(existing, "format", provider.Format); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
|
||
if err := SetYAMLMappingField(providerNodes, name, provider); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func FetchSubscriptionUserInfo(url string, userAgent string, retryTimes int) (string, error) {
|
||
client := Request(retryTimes)
|
||
defer client.Close()
|
||
resp, err := client.R().SetHeader("User-Agent", userAgent).Head(url)
|
||
if err != nil {
|
||
logger.Logger.Debug("创建 HEAD 请求失败", zap.Error(err))
|
||
return "", NewNetworkRequestError(url, err)
|
||
}
|
||
defer resp.Body.Close()
|
||
if userInfo := resp.Header().Get("subscription-userinfo"); userInfo != "" {
|
||
return userInfo, nil
|
||
}
|
||
|
||
logger.Logger.Debug("subscription-userinfo header not found in response")
|
||
return "", NewNetworkResponseError("subscription-userinfo header not found", nil)
|
||
}
|
||
|
||
// MergeSubAndTemplate 把“模板侧需要参与计算的最小叠加层”和“本项目生成结果”合并。
|
||
// 它只处理本项目关心的运行期结构,不负责最终 YAML 输出。
|
||
func MergeSubAndTemplate(temp *generatedConfig, sub *generatedConfig, igcg bool) {
|
||
var countryGroupNames []string
|
||
for _, proxyGroup := range sub.ProxyGroup {
|
||
if proxyGroup.IsCountry {
|
||
countryGroupNames = append(
|
||
countryGroupNames, proxyGroup.Name,
|
||
)
|
||
}
|
||
}
|
||
var proxyNames []string
|
||
for _, proxy := range sub.Proxy {
|
||
proxyNames = append(proxyNames, proxy.Name)
|
||
}
|
||
|
||
for i := range temp.ProxyGroup {
|
||
if temp.ProxyGroup[i].IsCountry {
|
||
continue
|
||
}
|
||
newProxies := make([]string, 0)
|
||
countryGroupMap := make(map[string]generatedGroup)
|
||
for _, v := range sub.ProxyGroup {
|
||
if v.IsCountry {
|
||
countryGroupMap[v.Name] = v
|
||
}
|
||
}
|
||
for j := range temp.ProxyGroup[i].Proxies {
|
||
reg := regexp.MustCompile("<(.*?)>")
|
||
if reg.Match([]byte(temp.ProxyGroup[i].Proxies[j])) {
|
||
key := reg.FindStringSubmatch(temp.ProxyGroup[i].Proxies[j])[1]
|
||
switch key {
|
||
case "all":
|
||
newProxies = append(newProxies, proxyNames...)
|
||
case "countries":
|
||
if !igcg {
|
||
newProxies = append(newProxies, countryGroupNames...)
|
||
}
|
||
default:
|
||
if !igcg {
|
||
if len(key) == 2 {
|
||
newProxies = append(
|
||
newProxies, countryGroupMap[GetContryName(key)].Proxies...,
|
||
)
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
newProxies = append(newProxies, temp.ProxyGroup[i].Proxies[j])
|
||
}
|
||
}
|
||
temp.ProxyGroup[i].Proxies = newProxies
|
||
}
|
||
if !igcg {
|
||
temp.ProxyGroup = append(temp.ProxyGroup, sub.ProxyGroup...)
|
||
}
|
||
}
|