Skip to content

Commit

Permalink
feat: Consolidate URL, Auth, and UserAgent into Proxy struct
Browse files Browse the repository at this point in the history
  • Loading branch information
presbrey committed Aug 10, 2024
1 parent 21c7f5e commit c87b751
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 102 deletions.
35 changes: 19 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,22 @@ import (

func main() {
config := multiproxy.Config{
ProxyURLs: []string{
"http://proxy1.example.com:8080",
"http://proxy2.example.com:8080",
Proxies: []multiproxy.Proxy{
{
URL: mustParseURL("http://proxy1.example.com:8080"),
Auth: &multiproxy.ProxyAuth{Username: "user1", Password: "pass1"},
},
{
URL: mustParseURL("http://proxy2.example.com:8080"),
Auth: &multiproxy.ProxyAuth{Username: "user2", Password: "pass2"},
},
},
ProxyAuth: map[string]multiproxy.ProxyAuth{
"http://proxy1.example.com:8080": {Username: "user1", Password: "pass1"},
"http://proxy2.example.com:8080": {Username: "user2", Password: "pass2"},
},
CookieTimeout: 10 * time.Minute,
CookieOptions: &cookiejar.Options{PublicSuffixList: publicsuffix.List},
DialTimeout: 30 * time.Second,
RequestTimeout: 1 * time.Minute,
RetryAttempts: 3,
RetryDelay: 5 * time.Second,
CookieTimeout: 10 * time.Minute,
CookieOptions: &cookiejar.Options{PublicSuffixList: publicsuffix.List},
DialTimeout: 30 * time.Second,
RequestTimeout: 1 * time.Minute,
RetryAttempts: 3,
RetryDelay: 5 * time.Second,
ProxyRotateCount: 10,
}

Expand All @@ -82,10 +84,11 @@ func main() {

The `Config` struct allows you to customize the behavior of the MultiProxy Client:

- `ProxyURLs`: List of proxy URLs to use
- `ProxyAuth`: Map of proxy URLs to their respective authentication credentials
- `Proxies`: List of Proxy structs, each containing:
- `URL`: The URL of the proxy
- `Auth`: Pointer to ProxyAuth struct with Username and Password
- `UserAgent`: User-Agent string for this specific proxy
- `ProxyRotateCount`: Number of requests after which to rotate to the next proxy
- `ProxyUserAgents`: Map of proxy URLs to their respective User-Agent strings

- `BackoffTime`: Time to wait before retrying a failed proxy
- `DialTimeout`: Timeout for establishing a connection to a proxy
Expand Down
10 changes: 5 additions & 5 deletions connect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,11 @@ func TestConnectProxyWithHTTPS(t *testing.T) {
}

config := Config{
ProxyURLs: []string{
proxyURL.String(),
},
ProxyAuth: map[string]ProxyAuth{
proxyURL.Host: {Username: "user", Password: "pass"},
Proxies: []Proxy{
{
URL: proxyURL,
Auth: &ProxyAuth{Username: "user", Password: "pass"},
},
},
DialTimeout: 5 * time.Second,
InsecureSkipVerify: true,
Expand Down
39 changes: 17 additions & 22 deletions errors_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package multiproxy

import (
"net/url"
"testing"
"time"

Expand All @@ -14,9 +15,9 @@ func TestAllProxiesUnavailable(t *testing.T) {

// Use non-routable IPs to simulate unavailable proxies
config := Config{
ProxyURLs: []string{
"socks5://10.255.255.1:1080",
"socks5://10.255.255.2:1080",
Proxies: []Proxy{
{URL: &url.URL{Scheme: "socks5", Host: "10.255.255.1:1080"}},
{URL: &url.URL{Scheme: "socks5", Host: "10.255.255.2:1080"}},
},
DialTimeout: 1 * time.Second,
DefaultUserAgent: "DefaultUserAgent/1.0",
Expand All @@ -41,7 +42,9 @@ func TestAllProxiesUnavailable(t *testing.T) {

func TestClientGetError(t *testing.T) {
config := Config{
ProxyURLs: []string{"http://10.255.255.1:8080"},
Proxies: []Proxy{
{URL: &url.URL{Scheme: "http", Host: "10.255.255.1:8080"}},
},
DialTimeout: 5 * time.Second,
}

Expand All @@ -54,7 +57,9 @@ func TestClientGetError(t *testing.T) {

func TestClientPostError(t *testing.T) {
config := Config{
ProxyURLs: []string{"http://10.255.255.1:8080"},
Proxies: []Proxy{
{URL: &url.URL{Scheme: "http", Host: "10"}},
},
DialTimeout: 5 * time.Second,
}

Expand All @@ -67,7 +72,9 @@ func TestClientPostError(t *testing.T) {

func TestClientHeadError(t *testing.T) {
config := Config{
ProxyURLs: []string{"http://10.255.255.1:8080"},
Proxies: []Proxy{
{URL: &url.URL{Scheme: "http", Host: "10"}},
},
DialTimeout: 5 * time.Second,
}

Expand All @@ -78,35 +85,23 @@ func TestClientHeadError(t *testing.T) {
assert.Contains(t, err.Error(), "invalid control character")
}

func TestClientNewError(t *testing.T) {
config := Config{
ProxyURLs: []string{"\000"},
DialTimeout: 5 * time.Second,
}

_, err := NewClient(config)
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid control character")
}

func TestClientWithNoProxyURLs(t *testing.T) {
config := Config{
ProxyURLs: []string{},
DialTimeout: 5 * time.Second,
Proxies: []Proxy{},
}

_, err := NewClient(config)
require.Error(t, err)
assert.Contains(t, err.Error(), "at least one proxy URL is required")
assert.Contains(t, err.Error(), "at least one proxy is required")
}

func TestDialTimeout(t *testing.T) {
// Use a non-routable IP address to simulate a timeout
nonRoutableIP := "10.255.255.1:8080"

config := Config{
ProxyURLs: []string{
"http://" + nonRoutableIP,
Proxies: []Proxy{
{URL: &url.URL{Scheme: "http", Host: nonRoutableIP}},
},
DialTimeout: 1 * time.Second,
RequestTimeout: 1 * time.Second,
Expand Down
62 changes: 32 additions & 30 deletions multiproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,21 @@ import (
"golang.org/x/sync/singleflight"
)

type Proxy struct {
URL *url.URL
Auth *ProxyAuth
UserAgent string
}

type ProxyAuth struct {
Username string
Password string
}

type Config struct {
// Proxy configuration
ProxyURLs []string
ProxyAuth map[string]ProxyAuth
Proxies []Proxy
ProxyRotateCount int
ProxyUserAgents map[string]string

// Timeouts and delays
BackoffTime time.Duration
Expand Down Expand Up @@ -75,24 +79,22 @@ type Client struct {
}

func NewClient(config Config) (*Client, error) {
if len(config.ProxyURLs) == 0 {
return nil, errors.New("at least one proxy URL is required")
if len(config.Proxies) == 0 {
return nil, errors.New("at least one proxy is required")
}

c := &Client{
config: config,
servers: make([]*url.URL, len(config.ProxyURLs)),
states: make([]proxyState, len(config.ProxyURLs)),
servers: make([]*url.URL, len(config.Proxies)),
states: make([]proxyState, len(config.Proxies)),
}

for i, proxyURL := range config.ProxyURLs {
serverURL, err := url.Parse(proxyURL)
if err != nil {
return nil, fmt.Errorf("invalid proxy URL %s: %v", proxyURL, err)
}
c.servers[i] = serverURL
for i, elt := range config.Proxies {
c.servers[i] = elt.URL

auth, hasAuth := c.config.ProxyAuth[serverURL.Host]
hasAuth := elt.Auth != nil &&
(elt.Auth.Username != "" ||
elt.Auth.Password != "")

var transport http.RoundTripper

Expand All @@ -103,17 +105,17 @@ func NewClient(config Config) (*Client, error) {
dialer.Timeout = c.config.DialTimeout
}

if serverURL.Scheme == "socks5" {
auth := &proxy.Auth{
User: auth.Username,
Password: auth.Password,
}
if !hasAuth {
auth = nil
if elt.URL.Scheme == "socks5" {
var auth *proxy.Auth
if hasAuth {
auth = &proxy.Auth{
User: elt.Auth.Username,
Password: elt.Auth.Password,
}
}
socksDialer, err := proxy.SOCKS5("tcp", serverURL.Host, auth, dialer)
socksDialer, err := proxy.SOCKS5("tcp", elt.URL.Host, auth, dialer)
if err != nil {
return nil, fmt.Errorf("failed to create SOCKS5 dialer for %s: %v", serverURL.Host, err)
return nil, fmt.Errorf("failed to create SOCKS5 dialer for %s: %v", elt.URL.Host, err)
}
transport = &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
Expand All @@ -125,7 +127,7 @@ func NewClient(config Config) (*Client, error) {
}
} else {
proxyURL := func(_ *http.Request) (*url.URL, error) {
return serverURL, nil
return elt.URL, nil
}
transport = &http.Transport{
Proxy: proxyURL,
Expand All @@ -136,12 +138,12 @@ func NewClient(config Config) (*Client, error) {
}
if hasAuth {
transport.(*http.Transport).ProxyConnectHeader = http.Header{
"Proxy-Authorization": {basicAuth(auth.Username, auth.Password)},
"Proxy-Authorization": {basicAuth(elt.Auth.Username, elt.Auth.Password)},
}
// Also set it for non-CONNECT requests
transport.(*http.Transport).Proxy = func(req *http.Request) (*url.URL, error) {
req.Header.Set("Proxy-Authorization", basicAuth(auth.Username, auth.Password))
return serverURL, nil
req.Header.Set("Proxy-Authorization", basicAuth(elt.Auth.Username, elt.Auth.Password))
return elt.URL, nil
}
}
}
Expand Down Expand Up @@ -189,15 +191,15 @@ func (c *Client) do(req *http.Request) (*http.Response, error) {
}

// Apply rate limiting
if limit, ok := c.config.RateLimits[c.servers[idx].Host]; ok {
if limit, ok := c.config.RateLimits[c.config.Proxies[idx].URL.Host]; ok {
if now.Sub(state.lastRequestAt) < limit {
time.Sleep(limit - now.Sub(state.lastRequestAt))
}
}

// Set proxy-specific User-Agent if configured
if userAgent, ok := c.config.ProxyUserAgents[c.servers[idx].Host]; ok {
req.Header.Set("User-Agent", userAgent)
if c.config.Proxies[idx].UserAgent != "" {
req.Header.Set("User-Agent", c.config.Proxies[idx].UserAgent)
}

// Set request timeout
Expand Down
Loading

0 comments on commit c87b751

Please sign in to comment.