package main
import (
"context"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"net/url"
"os"
"sync"
"time"
)
// Config holds the job configuration.
type Config struct {
PackageKey string
TargetURL string
Countries []string
MaxConcurrency int
MaxRetries int
Timeout time.Duration
}
// ProxyResult holds the outcome of a single geo-targeted request.
type ProxyResult struct {
Country string `json:"country"`
IP string `json:"ip,omitempty"`
ISP string `json:"isp,omitempty"`
City string `json:"city,omitempty"`
StatusCode int `json:"status_code"`
BodySize int `json:"body_size"`
Duration time.Duration `json:"duration"`
Attempts int `json:"attempts"`
Error string `json:"error,omitempty"`
}
// CheckerResponse matches the SOAX checker JSON structure.
type CheckerResponse struct {
Status bool `json:"status"`
Data struct {
IP string `json:"ip"`
CountryCode string `json:"country_code"`
CountryName string `json:"country_name"`
Region string `json:"region"`
City string `json:"city"`
ISP string `json:"isp"`
Carrier string `json:"carrier"`
} `json:"data"`
}
func main() {
cfg := Config{
PackageKey: os.Getenv("SOAX_PACKAGE_KEY"),
TargetURL: "https://checker.soax.com/api/ipinfo",
Countries: []string{
"us", "gb", "de", "fr", "jp",
"br", "au", "ca", "in", "kr",
},
MaxConcurrency: 5,
MaxRetries: 3,
Timeout: 30 * time.Second,
}
if cfg.PackageKey == "" {
fmt.Println("Set SOAX_PACKAGE_KEY environment variable")
os.Exit(1)
}
ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
defer cancel()
results := runJob(ctx, cfg)
output, _ := json.MarshalIndent(results, "", " ")
fmt.Println(string(output))
// Summary
succeeded := 0
for _, r := range results {
if r.Error == "" {
succeeded++
}
}
fmt.Printf("\n%d/%d countries succeeded\n", succeeded, len(results))
}
// runJob dispatches workers and collects results.
func runJob(ctx context.Context, cfg Config) []ProxyResult {
jobs := make(chan string, len(cfg.Countries))
results := make(chan ProxyResult, len(cfg.Countries))
// Start worker pool
var wg sync.WaitGroup
for i := 0; i < cfg.MaxConcurrency; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
worker(ctx, workerID, cfg, jobs, results)
}(i)
}
// Send jobs
for _, country := range cfg.Countries {
jobs <- country
}
close(jobs)
// Wait for all workers to finish, then close results
go func() {
wg.Wait()
close(results)
}()
// Collect results
var collected []ProxyResult
for r := range results {
collected = append(collected, r)
}
return collected
}
// worker pulls countries from the jobs channel and fetches each one.
func worker(ctx context.Context, id int, cfg Config, jobs <-chan string, results chan<- ProxyResult) {
for country := range jobs {
select {
case <-ctx.Done():
results <- ProxyResult{
Country: country,
Error: "cancelled: timeout exceeded",
}
return
default:
result := fetchWithRetry(ctx, cfg, country)
results <- result
}
}
}
// fetchWithRetry attempts the request up to MaxRetries with exponential backoff.
func fetchWithRetry(ctx context.Context, cfg Config, country string) ProxyResult {
var lastErr error
for attempt := 1; attempt <= cfg.MaxRetries; attempt++ {
result, err := fetchCountry(ctx, cfg, country)
if err == nil {
result.Attempts = attempt
return result
}
lastErr = err
// Don't sleep after the last attempt
if attempt < cfg.MaxRetries {
backoff := time.Duration(math.Pow(2, float64(attempt-1))) * 500 * time.Millisecond
select {
case <-ctx.Done():
return ProxyResult{
Country: country,
Attempts: attempt,
Error: "cancelled: timeout exceeded",
}
case <-time.After(backoff):
// continue to next attempt
}
}
}
return ProxyResult{
Country: country,
Attempts: cfg.MaxRetries,
Error: lastErr.Error(),
}
}
// fetchCountry makes a single proxied request for a given country.
func fetchCountry(ctx context.Context, cfg Config, country string) (ProxyResult, error) {
sessionID := fmt.Sprintf("geo_%s", country)
proxyStr := fmt.Sprintf("http://country-%s-session-%s:%s@proxy.soax.com:1337", country, sessionID, cfg.PackageKey)
proxyURL, err := url.Parse(proxyStr)
if err != nil {
return ProxyResult{}, fmt.Errorf("invalid proxy URL: %w", err)
}
client := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
Timeout: 10 * time.Second,
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, cfg.TargetURL, nil)
if err != nil {
return ProxyResult{}, fmt.Errorf("request creation failed: %w", err)
}
start := time.Now()
resp, err := client.Do(req)
duration := time.Since(start)
if err != nil {
return ProxyResult{}, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return ProxyResult{}, fmt.Errorf("body read failed: %w", err)
}
result := ProxyResult{
Country: country,
StatusCode: resp.StatusCode,
BodySize: len(body),
Duration: duration,
}
// Parse checker response to extract IP metadata
var checker CheckerResponse
if json.Unmarshal(body, &checker) == nil && checker.Status {
result.IP = checker.Data.IP
result.ISP = checker.Data.ISP
result.City = checker.Data.City
}
return result, nil
}