From 7677003308e8d874687d9e6dd9ff78af29349a99 Mon Sep 17 00:00:00 2001 From: spiritlhl <103393591+spiritLHLS@users.noreply.github.com> Date: Tue, 5 Aug 2025 08:12:52 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8A=A0=E5=85=A5=E9=87=8D=E8=AF=95?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=E9=81=BF=E5=85=8D=E4=B8=80=E6=AC=A1=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E5=A4=B1=E8=B4=A5=E5=88=99=E5=85=A8=E9=83=A8=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bgptools/pop.go | 174 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 132 insertions(+), 42 deletions(-) diff --git a/bgptools/pop.go b/bgptools/pop.go index 798a6ef..2eb24e1 100644 --- a/bgptools/pop.go +++ b/bgptools/pop.go @@ -7,6 +7,7 @@ import ( "net" "regexp" "strings" + "time" "github.com/google/uuid" "github.com/imroc/req/v3" @@ -41,14 +42,48 @@ type PoPResult struct { Result string } +// retryConfig 重试配置 +type retryConfig struct { + maxRetries int + timeouts []time.Duration +} + +// 默认重试配置:3次重试,超时时间分别为3s、4s、5s +var defaultRetryConfig = retryConfig{ + maxRetries: 3, + timeouts: []time.Duration{3 * time.Second, 4 * time.Second, 5 * time.Second}, +} + +// executeWithRetry 执行带重试的HTTP请求 +func executeWithRetry(client *req.Client, url string, config retryConfig) (*req.Response, error) { + var lastErr error + for attempt := 0; attempt < config.maxRetries; attempt++ { + timeout := config.timeouts[attempt] + resp, err := client.SetTimeout(timeout).R(). + Get(url) + if err == nil && resp.StatusCode == 200 { + return resp, nil + } + if err != nil { + lastErr = fmt.Errorf("attempt %d failed with timeout %v: %w", attempt+1, timeout, err) + } else { + lastErr = fmt.Errorf("attempt %d failed with HTTP status %d (timeout %v)", attempt+1, resp.StatusCode, timeout) + } + if attempt < config.maxRetries-1 { + time.Sleep(3 * time.Second) + } + } + return nil, fmt.Errorf("all %d attempts failed, last error: %w", config.maxRetries, lastErr) +} + func getISPAbbr(asn, name string) string { if abbr, ok := model.Tier1Global[asn]; ok { return abbr } - if idx := strings.Index(name, " "); idx != -1 && idx > 18 { + if idx := strings.Index(name, " "); idx != -1 && idx >= 18 { return name[:idx] } - return name + return strings.TrimSpace(name) } func getISPType(asn string, tier1 bool, direct bool) string { @@ -74,42 +109,54 @@ func isValidIP(ip string) bool { return net.ParseIP(ip) != nil } -func getSVGPath(client *req.Client, ip string) (string, error) { +func getSVGPath(ip string) (string, error) { if !isValidIP(ip) { return "", fmt.Errorf("invalid IP address: %s", ip) } - url := fmt.Sprintf("https://bgp.tools/prefix/%s#connectivity", ip) - resp, err := client.R().Get(url) - if err != nil { - return "", fmt.Errorf("failed to fetch BGP info for IP %s: %w", ip, err) + var lastErr error + for attempt := 0; attempt < defaultRetryConfig.maxRetries; attempt++ { + client := req.C().ImpersonateChrome() + url := fmt.Sprintf("https://bgp.tools/prefix/%s#connectivity", ip) + resp, err := executeWithRetry(client, url, defaultRetryConfig) + if err == nil { + body := resp.String() + re := regexp.MustCompile(`]+id="pathimg"[^>]+src="([^"]+)"`) + matches := re.FindStringSubmatch(body) + if len(matches) >= 2 { + return matches[1], nil + } + lastErr = fmt.Errorf("SVG path not found for IP %s", ip) + } else { + lastErr = fmt.Errorf("failed to fetch BGP info for IP %s: %w", ip, err) + } + if attempt < defaultRetryConfig.maxRetries-1 { + time.Sleep(1 * time.Second) + } } - if resp.StatusCode != 200 { - return "", fmt.Errorf("HTTP error %d when fetching BGP info for IP %s", resp.StatusCode, ip) - } - body := resp.String() - re := regexp.MustCompile(`]+id="pathimg"[^>]+src="([^"]+)"`) - matches := re.FindStringSubmatch(body) - if len(matches) < 2 { - return "", fmt.Errorf("SVG path not found for IP %s", ip) - } - return matches[1], nil + return "", fmt.Errorf("failed to get SVG path after %d retries: %w", defaultRetryConfig.maxRetries, lastErr) } -func downloadSVG(client *req.Client, svgPath string) (string, error) { - uuid := uuid.NewString() - url := fmt.Sprintf("https://bgp.tools%s?%s&loggedin", svgPath, uuid) - resp, err := client.R().Get(url) - if err != nil { - return "", fmt.Errorf("failed to download SVG: %w", err) +func downloadSVG(svgPath string) (string, error) { + var lastErr error + for attempt := 0; attempt < defaultRetryConfig.maxRetries; attempt++ { + client := req.C().ImpersonateChrome() + uuid := uuid.NewString() + url := fmt.Sprintf("https://bgp.tools%s?%s&loggedin", svgPath, uuid) + resp, err := executeWithRetry(client, url, defaultRetryConfig) + if err == nil { + bodyBytes, err := io.ReadAll(resp.Body) + if err == nil { + return string(bodyBytes), nil + } + lastErr = fmt.Errorf("failed to read SVG response body: %w", err) + } else { + lastErr = fmt.Errorf("failed to download SVG: %w", err) + } + if attempt < defaultRetryConfig.maxRetries-1 { + time.Sleep(1 * time.Second) + } } - if resp.StatusCode != 200 { - return "", fmt.Errorf("HTTP error %d when downloading SVG", resp.StatusCode) - } - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read SVG response body: %w", err) - } - return string(bodyBytes), nil + return "", fmt.Errorf("failed to download SVG after %d retries: %w", defaultRetryConfig.maxRetries, lastErr) } func parseASAndEdges(svg string) ([]ASCard, []Arrow) { @@ -183,6 +230,7 @@ func findUpstreams(targetASN string, nodes []ASCard, edges []Arrow) []Upstream { } } var upstreams []Upstream + addedASNs := map[string]bool{} for _, n := range nodes { if !upstreamMap[n.ASN] { continue @@ -196,6 +244,7 @@ func findUpstreams(targetASN string, nodes []ASCard, edges []Arrow) []Upstream { Tier1: isTier1, Type: upstreamType, }) + addedASNs[n.ASN] = true } if len(upstreams) == 1 { currentASN := upstreams[0].ASN @@ -214,14 +263,7 @@ func findUpstreams(targetASN string, nodes []ASCard, edges []Arrow) []Upstream { nextASN = asn break } - found := false - for _, existing := range upstreams { - if existing.ASN == nextASN { - found = true - break - } - } - if found { + if addedASNs[nextASN] { break } var nextNode *ASCard @@ -243,8 +285,56 @@ func findUpstreams(targetASN string, nodes []ASCard, edges []Arrow) []Upstream { Tier1: isTier1, Type: upstreamType, }) + addedASNs[nextNode.ASN] = true currentASN = nextASN } + } else if len(upstreams) > 1 { + for _, directUpstream := range upstreams { + currentASN := directUpstream.ASN + for { + nextUpstreams := map[string]bool{} + for _, e := range edges { + if e.From == currentASN { + nextUpstreams[e.To] = true + } + } + if len(nextUpstreams) != 1 { + break + } + var nextASN string + for asn := range nextUpstreams { + nextASN = asn + break + } + if addedASNs[nextASN] { + break + } + var nextNode *ASCard + for _, n := range nodes { + if n.ASN == nextASN { + nextNode = &n + break + } + } + if nextNode == nil { + break + } + isTier1 := (nextNode.Fill == "white" && nextNode.Stroke == "#005ea5") + if isTier1 { + upstreamType := getISPType(nextNode.ASN, isTier1, false) + upstreams = append(upstreams, Upstream{ + ASN: nextNode.ASN, + Name: nextNode.Name, + Direct: false, + Tier1: isTier1, + Type: upstreamType, + }) + addedASNs[nextNode.ASN] = true + break + } + currentASN = nextASN + } + } } return upstreams } @@ -253,12 +343,12 @@ func GetPoPInfo(ip string) (*PoPResult, error) { if ip == "" { return nil, fmt.Errorf("IP address cannot be empty") } - client := req.C().ImpersonateChrome() - svgPath, err := getSVGPath(client, ip) + + svgPath, err := getSVGPath(ip) if err != nil { return nil, fmt.Errorf("获取SVG路径失败: %w", err) } - svg, err := downloadSVG(client, svgPath) + svg, err := downloadSVG(svgPath) if err != nil { return nil, fmt.Errorf("下载SVG失败: %w", err) }