package proxy import ( "context" "fmt" "math/rand/v2" "net/http" "net/url" "os" "strings" "github.com/anotherhadi/iknowyou/config" ) type httpClientKey struct{} type proxychainsConfKey struct{} // WithClient injects a proxy-aware HTTP client into the context. func WithClient(ctx context.Context, client *http.Client) context.Context { return context.WithValue(ctx, httpClientKey{}, client) } // ClientFromContext returns the proxy-aware HTTP client stored in ctx, // or http.DefaultClient if none was set. func ClientFromContext(ctx context.Context) *http.Client { if client, ok := ctx.Value(httpClientKey{}).(*http.Client); ok && client != nil { return client } return http.DefaultClient } // WithProxychainsConf injects a proxychains config file path into the context. func WithProxychainsConf(ctx context.Context, confPath string) context.Context { return context.WithValue(ctx, proxychainsConfKey{}, confPath) } // ProxychainsConfFromContext returns the proxychains config file path stored in // ctx, or an empty string if none was set. func ProxychainsConfFromContext(ctx context.Context) string { if path, ok := ctx.Value(proxychainsConfKey{}).(string); ok { return path } return "" } // fallbackTransport is an http.RoundTripper that tries proxies in random order // and falls back to the next one on network error. type fallbackTransport struct { transports []*http.Transport } func (t *fallbackTransport) RoundTrip(req *http.Request) (*http.Response, error) { order := rand.Perm(len(t.transports)) var lastErr error for _, i := range order { resp, err := t.transports[i].RoundTrip(req) if err == nil { return resp, nil } lastErr = err } return nil, lastErr } // NewClient builds an *http.Client that routes requests through the given // proxies, trying them in random order and falling back on network error. // Returns nil if proxies is empty. func NewClient(proxies []config.ProxyEntry) (*http.Client, error) { if len(proxies) == 0 { return nil, nil } transports := make([]*http.Transport, 0, len(proxies)) for _, p := range proxies { u, err := url.Parse(p.URL) if err != nil { return nil, fmt.Errorf("proxy: invalid URL %q: %w", p.URL, err) } transports = append(transports, &http.Transport{ Proxy: http.ProxyURL(u), }) } return &http.Client{ Transport: &fallbackTransport{transports: transports}, }, nil } // WriteProxychainsConf generates a temporary proxychains4 config file from the // given proxy list and returns its path along with a cleanup function. // Returns ("", nil, nil) if proxies is empty. func WriteProxychainsConf(proxies []config.ProxyEntry) (string, func(), error) { if len(proxies) == 0 { return "", func() {}, nil } var sb strings.Builder sb.WriteString("dynamic_chain\nproxy_dns\n\n[ProxyList]\n") for _, p := range proxies { u, err := url.Parse(p.URL) if err != nil { return "", nil, fmt.Errorf("proxy: invalid URL %q: %w", p.URL, err) } scheme := u.Scheme // proxychains only knows socks4, socks5, http if scheme != "socks4" && scheme != "socks5" && scheme != "http" { scheme = "socks5" } host := u.Hostname() port := u.Port() if port == "" { port = defaultPort(scheme) } line := fmt.Sprintf("%s %s %s", scheme, host, port) if u.User != nil { user := u.User.Username() pass, _ := u.User.Password() if user != "" { line += " " + user if pass != "" { line += " " + pass } } } sb.WriteString(line + "\n") } f, err := os.CreateTemp("", "iky-proxychains-*.conf") if err != nil { return "", nil, fmt.Errorf("proxy: create temp conf: %w", err) } if _, err := f.WriteString(sb.String()); err != nil { _ = f.Close() _ = os.Remove(f.Name()) return "", nil, fmt.Errorf("proxy: write conf: %w", err) } _ = f.Close() path := f.Name() cleanup := func() { _ = os.Remove(path) } return path, cleanup, nil } func defaultPort(scheme string) string { switch scheme { case "socks4", "socks5": return "1080" case "http": return "8080" } return "1080" }