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{ ipInfoURL: defaultIPInfoURL, insecureSkipVerify: insecure, allowPrivateHosts: allowPrivate, validateResolvedIP: validateResolvedIP, } } const ( defaultIPInfoURL = "http://ip-api.com/json/?lang=zh-CN" defaultProxyProbeTimeout = 30 * time.Second ) type proxyProbeService struct { ipInfoURL string 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) } startTime := time.Now() req, err := http.NewRequestWithContext(ctx, "GET", s.ipInfoURL, 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) } 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"` } body, err := io.ReadAll(resp.Body) if err != nil { return nil, latencyMs, fmt.Errorf("failed to read response: %w", err) } if err := json.Unmarshal(body, &ipInfo); err != nil { return nil, latencyMs, fmt.Errorf("failed to parse response: %w", err) } 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 }