package repository import ( "context" "encoding/json" "fmt" "io" "log" "net/http" "strings" "time" "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/httpclient" "github.com/Wei-Shaw/sub2api/internal/service" ) func NewProxyExitInfoProber(cfg *config.Config) service.ProxyExitInfoProber { insecure := false allowPrivate := false validateResolvedIP := true if cfg != nil { insecure = cfg.Security.ProxyProbe.InsecureSkipVerify allowPrivate = cfg.Security.URLAllowlist.AllowPrivateHosts validateResolvedIP = cfg.Security.URLAllowlist.Enabled } if insecure { log.Printf("[ProxyProbe] Warning: insecure_skip_verify is not allowed and will cause probe failure.") } return &proxyProbeService{ insecureSkipVerify: insecure, allowPrivateHosts: allowPrivate, validateResolvedIP: validateResolvedIP, } } const ( defaultProxyProbeTimeout = 30 * time.Second ) // probeURLs 按优先级排列的探测 URL 列表 // 某些 AI API 专用代理只允许访问特定域名,因此需要多个备选 var probeURLs = []struct { url string parser string // "ip-api" or "httpbin" }{ {"http://ip-api.com/json/?lang=zh-CN", "ip-api"}, {"http://httpbin.org/ip", "httpbin"}, } type proxyProbeService struct { insecureSkipVerify bool allowPrivateHosts bool validateResolvedIP bool } func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*service.ProxyExitInfo, int64, error) { client, err := httpclient.GetClient(httpclient.Options{ ProxyURL: proxyURL, Timeout: defaultProxyProbeTimeout, InsecureSkipVerify: s.insecureSkipVerify, ProxyStrict: true, ValidateResolvedIP: s.validateResolvedIP, AllowPrivateHosts: s.allowPrivateHosts, }) if err != nil { return nil, 0, fmt.Errorf("failed to create proxy client: %w", err) } var lastErr error for _, probe := range probeURLs { exitInfo, latencyMs, err := s.probeWithURL(ctx, client, probe.url, probe.parser) if err == nil { return exitInfo, latencyMs, nil } lastErr = err } return nil, 0, fmt.Errorf("all probe URLs failed, last error: %w", lastErr) } func (s *proxyProbeService) probeWithURL(ctx context.Context, client *http.Client, url string, parser string) (*service.ProxyExitInfo, int64, error) { startTime := time.Now() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, 0, fmt.Errorf("failed to create request: %w", err) } resp, err := client.Do(req) if err != nil { return nil, 0, fmt.Errorf("proxy connection failed: %w", err) } defer func() { _ = resp.Body.Close() }() latencyMs := time.Since(startTime).Milliseconds() if resp.StatusCode != http.StatusOK { return nil, latencyMs, fmt.Errorf("request failed with status: %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, latencyMs, fmt.Errorf("failed to read response: %w", err) } switch parser { case "ip-api": return s.parseIPAPI(body, latencyMs) case "httpbin": return s.parseHTTPBin(body, latencyMs) default: return nil, latencyMs, fmt.Errorf("unknown parser: %s", parser) } } func (s *proxyProbeService) parseIPAPI(body []byte, latencyMs int64) (*service.ProxyExitInfo, int64, error) { var ipInfo struct { Status string `json:"status"` Message string `json:"message"` Query string `json:"query"` City string `json:"city"` Region string `json:"region"` RegionName string `json:"regionName"` Country string `json:"country"` CountryCode string `json:"countryCode"` } if err := json.Unmarshal(body, &ipInfo); err != nil { preview := string(body) if len(preview) > 200 { preview = preview[:200] + "..." } return nil, latencyMs, fmt.Errorf("failed to parse response: %w (body: %s)", err, preview) } if strings.ToLower(ipInfo.Status) != "success" { if ipInfo.Message == "" { ipInfo.Message = "ip-api request failed" } return nil, latencyMs, fmt.Errorf("ip-api request failed: %s", ipInfo.Message) } region := ipInfo.RegionName if region == "" { region = ipInfo.Region } return &service.ProxyExitInfo{ IP: ipInfo.Query, City: ipInfo.City, Region: region, Country: ipInfo.Country, CountryCode: ipInfo.CountryCode, }, latencyMs, nil } func (s *proxyProbeService) parseHTTPBin(body []byte, latencyMs int64) (*service.ProxyExitInfo, int64, error) { // httpbin.org/ip 返回格式: {"origin": "1.2.3.4"} var result struct { Origin string `json:"origin"` } if err := json.Unmarshal(body, &result); err != nil { return nil, latencyMs, fmt.Errorf("failed to parse httpbin response: %w", err) } if result.Origin == "" { return nil, latencyMs, fmt.Errorf("httpbin: no IP found in response") } return &service.ProxyExitInfo{ IP: result.Origin, }, latencyMs, nil }