mirror of
				https://github.com/oneclickvirt/backtrace.git
				synced 2025-11-04 15:52:37 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			276 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			276 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package backtrace
 | 
						||
 | 
						||
import (
 | 
						||
	"encoding/json"
 | 
						||
	"fmt"
 | 
						||
	"io"
 | 
						||
	"strings"
 | 
						||
	"time"
 | 
						||
 | 
						||
	"github.com/imroc/req/v3"
 | 
						||
	"github.com/oneclickvirt/backtrace/model"
 | 
						||
	. "github.com/oneclickvirt/defaultset"
 | 
						||
)
 | 
						||
 | 
						||
type Result struct {
 | 
						||
	i int
 | 
						||
	s string
 | 
						||
}
 | 
						||
 | 
						||
// removeDuplicates 切片去重
 | 
						||
func removeDuplicates(elements []string) []string {
 | 
						||
	if elements == nil {
 | 
						||
		return nil
 | 
						||
	}
 | 
						||
	seen := make(map[string]struct{})
 | 
						||
	var result []string
 | 
						||
	for _, v := range elements {
 | 
						||
		if _, ok := seen[v]; !ok {
 | 
						||
			seen[v] = struct{}{}
 | 
						||
			result = append(result, v)
 | 
						||
		}
 | 
						||
	}
 | 
						||
	return result
 | 
						||
}
 | 
						||
 | 
						||
// checkCdn 检查CDN可用性,参考shell脚本的测试逻辑
 | 
						||
func checkCdn(testUrl string) string {
 | 
						||
	client := req.C()
 | 
						||
	client.SetTimeout(6 * time.Second)
 | 
						||
	if model.EnableLoger {
 | 
						||
		InitLogger()
 | 
						||
		defer Logger.Sync()
 | 
						||
	}
 | 
						||
	for _, cdnUrl := range model.CdnList {
 | 
						||
		url := cdnUrl + testUrl
 | 
						||
		if model.EnableLoger {
 | 
						||
			Logger.Info(fmt.Sprintf("Testing CDN: %s", url))
 | 
						||
		}
 | 
						||
		resp, err := client.R().Get(url)
 | 
						||
		if err == nil {
 | 
						||
			b, err := io.ReadAll(resp.Body)
 | 
						||
			resp.Body.Close()
 | 
						||
			if err == nil && strings.Contains(string(b), "success") {
 | 
						||
				if model.EnableLoger {
 | 
						||
					Logger.Info(fmt.Sprintf("CDN available: %s", cdnUrl))
 | 
						||
				}
 | 
						||
				return cdnUrl
 | 
						||
			}
 | 
						||
		}
 | 
						||
		if model.EnableLoger {
 | 
						||
			Logger.Info(fmt.Sprintf("CDN test failed: %s, error: %v", cdnUrl, err))
 | 
						||
		}
 | 
						||
		time.Sleep(500 * time.Millisecond)
 | 
						||
	}
 | 
						||
	if model.EnableLoger {
 | 
						||
		Logger.Info("No CDN available, using direct connection")
 | 
						||
	}
 | 
						||
	return ""
 | 
						||
}
 | 
						||
 | 
						||
// getData 获取目标地址的文本内容
 | 
						||
func getData(endpoint string) string {
 | 
						||
	client := req.C()
 | 
						||
	client.SetTimeout(6 * time.Second)
 | 
						||
	client.R().
 | 
						||
		SetRetryCount(2).
 | 
						||
		SetRetryBackoffInterval(1*time.Second, 5*time.Second).
 | 
						||
		SetRetryFixedInterval(2 * time.Second)
 | 
						||
	if model.EnableLoger {
 | 
						||
		InitLogger()
 | 
						||
		defer Logger.Sync()
 | 
						||
	}
 | 
						||
	
 | 
						||
	// 先测试CDN可用性
 | 
						||
	testUrl := "https://raw.githubusercontent.com/spiritLHLS/ecs/main/back/test"
 | 
						||
	cdnUrl := checkCdn(testUrl)
 | 
						||
	
 | 
						||
	// 如果有可用的CDN,使用CDN获取数据
 | 
						||
	if cdnUrl != "" {
 | 
						||
		url := cdnUrl + endpoint
 | 
						||
		if model.EnableLoger {
 | 
						||
			Logger.Info(fmt.Sprintf("Using CDN: %s", url))
 | 
						||
		}
 | 
						||
		resp, err := client.R().Get(url)
 | 
						||
		if err == nil {
 | 
						||
			defer resp.Body.Close()
 | 
						||
			b, err := io.ReadAll(resp.Body)
 | 
						||
			if err == nil && !strings.Contains(string(b), "error") {
 | 
						||
				if model.EnableLoger {
 | 
						||
					Logger.Info(fmt.Sprintf("Received data length: %d", len(b)))
 | 
						||
				}
 | 
						||
				return string(b)
 | 
						||
			}
 | 
						||
		}
 | 
						||
		if model.EnableLoger {
 | 
						||
			Logger.Info(fmt.Sprintf("CDN request failed: %v", err))
 | 
						||
		}
 | 
						||
	}
 | 
						||
	
 | 
						||
	// CDN不可用,尝试直连
 | 
						||
	if model.EnableLoger {
 | 
						||
		Logger.Info(fmt.Sprintf("Trying direct connection: %s", endpoint))
 | 
						||
	}
 | 
						||
	resp, err := client.R().Get(endpoint)
 | 
						||
	if err == nil {
 | 
						||
		defer resp.Body.Close()
 | 
						||
		b, err := io.ReadAll(resp.Body)
 | 
						||
		if err == nil {
 | 
						||
			if model.EnableLoger {
 | 
						||
				Logger.Info(fmt.Sprintf("Received data length: %d", len(b)))
 | 
						||
			}
 | 
						||
			return string(b)
 | 
						||
		}
 | 
						||
	}
 | 
						||
	if model.EnableLoger {
 | 
						||
		Logger.Info(fmt.Sprintf("Direct connection failed: %v", err))
 | 
						||
	}
 | 
						||
	return ""
 | 
						||
}
 | 
						||
 | 
						||
// parseIcmpTargets 解析ICMP目标数据
 | 
						||
func parseIcmpTargets(jsonData string) []model.IcmpTarget {
 | 
						||
	var targets []model.IcmpTarget
 | 
						||
	err := json.Unmarshal([]byte(jsonData), &targets)
 | 
						||
	if err != nil {
 | 
						||
		if model.EnableLoger {
 | 
						||
			Logger.Info(fmt.Sprintf("解析ICMP目标失败: %s", err.Error()))
 | 
						||
		}
 | 
						||
		return nil
 | 
						||
	}
 | 
						||
	return targets
 | 
						||
}
 | 
						||
 | 
						||
// tryAlternativeIPs 从IcmpTargets获取备选IP地址
 | 
						||
func tryAlternativeIPs(targetName string, ipVersion string) []string {
 | 
						||
	if model.ParsedIcmpTargets == nil || (model.ParsedIcmpTargets != nil && len(model.ParsedIcmpTargets) == 0) {
 | 
						||
		return nil
 | 
						||
	}
 | 
						||
	if model.EnableLoger {
 | 
						||
		Logger.Info(fmt.Sprintf("使用备选地址: %s %s", targetName, ipVersion))
 | 
						||
	}
 | 
						||
	// 从目标名称中提取省份和ISP信息
 | 
						||
	var targetProvince, targetISP string
 | 
						||
	if strings.Contains(targetName, "北京") {
 | 
						||
		targetProvince = "北京"
 | 
						||
	} else if strings.Contains(targetName, "上海") {
 | 
						||
		targetProvince = "上海"
 | 
						||
	} else if strings.Contains(targetName, "广州") {
 | 
						||
		targetProvince = "广东"
 | 
						||
	} else if strings.Contains(targetName, "成都") {
 | 
						||
		targetProvince = "四川"
 | 
						||
	}
 | 
						||
	if strings.Contains(targetName, "电信") {
 | 
						||
		targetISP = "电信"
 | 
						||
	} else if strings.Contains(targetName, "联通") {
 | 
						||
		targetISP = "联通"
 | 
						||
	} else if strings.Contains(targetName, "移动") {
 | 
						||
		targetISP = "移动"
 | 
						||
	}
 | 
						||
	// 如果没有提取到信息,返回空
 | 
						||
	if targetProvince == "" || targetISP == "" {
 | 
						||
		return nil
 | 
						||
	}
 | 
						||
	// 查找匹配条件的目标
 | 
						||
	var result []string
 | 
						||
	for _, target := range model.ParsedIcmpTargets {
 | 
						||
		// 检查省份是否匹配(可能带有"省"字或不带)
 | 
						||
		provinceMatch := (target.Province == targetProvince) || (target.Province == targetProvince+"省")
 | 
						||
		// 检查ISP和IP版本是否匹配
 | 
						||
		if provinceMatch && target.ISP == targetISP && target.IPVersion == ipVersion {
 | 
						||
			// 解析IP列表
 | 
						||
			if target.IPs != "" {
 | 
						||
				ips := strings.Split(target.IPs, ",")
 | 
						||
				// 最多返回3个IP地址
 | 
						||
				count := 0
 | 
						||
				for _, ip := range ips {
 | 
						||
					if ip != "" {
 | 
						||
						result = append(result, strings.TrimSpace(ip))
 | 
						||
						count++
 | 
						||
						if count >= 3 {
 | 
						||
							break
 | 
						||
						}
 | 
						||
					}
 | 
						||
				}
 | 
						||
				if len(result) > 0 {
 | 
						||
					return result
 | 
						||
				}
 | 
						||
			}
 | 
						||
		}
 | 
						||
	}
 | 
						||
	return nil
 | 
						||
}
 | 
						||
 | 
						||
// mergeHops 合并多个hops结果
 | 
						||
func mergeHops(allHops [][]*Hop) []*Hop {
 | 
						||
	if len(allHops) == 0 {
 | 
						||
		return nil
 | 
						||
	}
 | 
						||
	if len(allHops) == 1 {
 | 
						||
		return allHops[0]
 | 
						||
	}
 | 
						||
	// 找到最大长度
 | 
						||
	maxLen := 0
 | 
						||
	for _, hops := range allHops {
 | 
						||
		if len(hops) > maxLen {
 | 
						||
			maxLen = len(hops)
 | 
						||
		}
 | 
						||
	}
 | 
						||
	var mergedHops []*Hop
 | 
						||
	// 逐位置合并
 | 
						||
	for pos := 0; pos < maxLen; pos++ {
 | 
						||
		var availableHops []*Hop
 | 
						||
		// 收集当前位置所有可用的hop
 | 
						||
		for _, hops := range allHops {
 | 
						||
			if pos < len(hops) {
 | 
						||
				availableHops = append(availableHops, hops[pos])
 | 
						||
			}
 | 
						||
		}
 | 
						||
		if len(availableHops) == 0 {
 | 
						||
			continue
 | 
						||
		}
 | 
						||
		// 如果只有一个可用hop,直接使用
 | 
						||
		if len(availableHops) == 1 {
 | 
						||
			mergedHops = append(mergedHops, availableHops[0])
 | 
						||
			continue
 | 
						||
		}
 | 
						||
		// 统计相同的hop(通过比较第一个node的IP)
 | 
						||
		hopCount := make(map[string][]*Hop)
 | 
						||
		for _, hop := range availableHops {
 | 
						||
			var key string
 | 
						||
			if len(hop.Nodes) > 0 && hop.Nodes[0].IP != nil {
 | 
						||
				key = hop.Nodes[0].IP.String()
 | 
						||
			} else {
 | 
						||
				key = "*" // 超时或无响应
 | 
						||
			}
 | 
						||
			if _, exists := hopCount[key]; !exists {
 | 
						||
				hopCount[key] = make([]*Hop, 0)
 | 
						||
			}
 | 
						||
			hopCount[key] = append(hopCount[key], hop)
 | 
						||
		}
 | 
						||
		// 按多数原则选择hop
 | 
						||
		if len(hopCount) == 1 {
 | 
						||
			// 所有hop都相同,选择第一个
 | 
						||
			mergedHops = append(mergedHops, availableHops[0])
 | 
						||
		} else {
 | 
						||
			// 找出最多的hop类型
 | 
						||
			maxCount := 0
 | 
						||
			var majorityHops []*Hop
 | 
						||
			for _, hops := range hopCount {
 | 
						||
				if len(hops) > maxCount {
 | 
						||
					maxCount = len(hops)
 | 
						||
					majorityHops = hops
 | 
						||
				}
 | 
						||
			}
 | 
						||
			// 如果有多数派,使用多数派的第一个
 | 
						||
			if maxCount > 1 || len(hopCount) == 2 {
 | 
						||
				mergedHops = append(mergedHops, majorityHops[0])
 | 
						||
			} else {
 | 
						||
				// 三个都不同,按请求早晚顺序选择第一个
 | 
						||
				mergedHops = append(mergedHops, availableHops[0])
 | 
						||
			}
 | 
						||
		}
 | 
						||
	}
 | 
						||
	return mergedHops
 | 
						||
}
 |