diff --git a/.github/workflows/test_golang.yml b/.github/workflows/test_golang.yml index 4539c136..f92d19ee 100644 --- a/.github/workflows/test_golang.yml +++ b/.github/workflows/test_golang.yml @@ -33,6 +33,11 @@ jobs: run: | docker run -d -p 1087:1080 serjs/go-socks5-proxy + - name: Start SOCKS4 Proxy (only on ubuntu) + if: matrix.platform == 'ubuntu-latest' + run: | + docker run -d -p 1088:1080 your/socks4-proxy-image + - name: Integration Tests working-directory: ./cycletls run: go test --race -v -tags=integration ./... \ No newline at end of file diff --git a/cycletls/connect.go b/cycletls/connect.go index 6cd5f9bd..e7af6a56 100644 --- a/cycletls/connect.go +++ b/cycletls/connect.go @@ -8,16 +8,30 @@ import ( "encoding/base64" "errors" "fmt" - http "github.com/Danny-Dasilva/fhttp" - http2 "github.com/Danny-Dasilva/fhttp/http2" - "golang.org/x/net/proxy" "io" "net" "net/url" "strconv" "sync" + + http "github.com/Danny-Dasilva/fhttp" + http2 "github.com/Danny-Dasilva/fhttp/http2" + "golang.org/x/net/proxy" + "h12.io/socks" ) +type SocksDialer struct { + socksDial func(string, string) (net.Conn, error) +} + +func (d *SocksDialer) DialContext(_ context.Context, network, addr string) (net.Conn, error) { + return d.socksDial(network, addr) +} + +func (d *SocksDialer) Dial(network, addr string) (net.Conn, error) { + return d.socksDial(network, addr) +} + // connectDialer allows to configure one-time use HTTP CONNECT client type connectDialer struct { ProxyURL url.URL @@ -64,7 +78,7 @@ func newConnectDialer(proxyURLStr string, UserAgent string) (proxy.ContextDialer if proxyURL.Port() == "" { proxyURL.Host = net.JoinHostPort(proxyURL.Host, "443") } - case "socks5": + case "socks5", "socks5h": var auth *proxy.Auth if proxyURL.User != nil { if proxyURL.User.Username() != "" { @@ -73,7 +87,11 @@ func newConnectDialer(proxyURLStr string, UserAgent string) (proxy.ContextDialer auth = &proxy.Auth{User: username, Password: password} } } - dialSocksProxy, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, nil) + var forward proxy.Dialer + if proxyURL.Scheme == "socks5h" { + forward = proxy.Direct + } + dialSocksProxy, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, forward) if err != nil { return nil, fmt.Errorf("Error creating SOCKS5 proxy, reason %s", err) } @@ -84,6 +102,12 @@ func newConnectDialer(proxyURLStr string, UserAgent string) (proxy.ContextDialer } client.DefaultHeader.Set("User-Agent", UserAgent) return client, nil + case "socks4": + var dialer *SocksDialer + dialer = &SocksDialer{socks.DialSocksProxy(socks.SOCKS4, proxyURL.Host)} + client.Dialer = dialer + client.DefaultHeader.Set("User-Agent", UserAgent) + return client, nil case "": return nil, errors.New("specify scheme explicitly (https://)") default: @@ -121,7 +145,7 @@ type ContextKeyHeader struct{} // ctx.Value will be inspected for optional ContextKeyHeader{} key, with `http.Header` value, // which will be added to outgoing request headers, overriding any colliding c.DefaultHeader func (c *connectDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - if c.ProxyURL.Scheme == "socks5" { + if c.ProxyURL.Scheme == "socks5" || c.ProxyURL.Scheme == "socks4" { return c.Dialer.DialContext(ctx, network, address) } diff --git a/cycletls/tests/integration/proxy_test.go b/cycletls/tests/integration/proxy_test.go index b33ae3d8..7246e6ff 100644 --- a/cycletls/tests/integration/proxy_test.go +++ b/cycletls/tests/integration/proxy_test.go @@ -34,23 +34,50 @@ func TestProxySuccess(t *testing.T) { } log.Print("Body: " + resp.Body) } +func TestSocks4Proxy(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("Skipping this test on non-linux platforms") + return + } + client := cycletls.Init() + resp, err := client.Do("https://ipinfo.io/json", cycletls.Options{ + Body: "", + Ja3: "771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-51-57-47-53-10,0-23-65281-10-11-35-16-5-51-43-13-45-28-21,29-23-24-25-256-257,0", + UserAgent: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0", + Proxy: "socks4://abc:123@127.0.0.1:1087", + Headers: map[string]string{ + "Accept": "Application/json, text/plain, */*", + }, + }, "GET") + if err != nil { + t.Fatalf("Request Failed: " + err.Error()) + } + if resp.Status != 200 { + t.Fatalf("Expected %d Got %d for Status", 200, resp.Status) + } + log.Print("Body: " + resp.Body) +} -// func TestHttpError(t *testing.T) { - -// client := cycletls.Init() -// resp, err := client.Do("https://ipinfo.io/json", cycletls.Options{ -// Body: "", -// Ja3: "771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-51-57-47-53-10,0-23-65281-10-11-35-16-5-51-43-13-45-28-21,29-23-24-25-256-257,0", -// UserAgent: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0", -// Proxy: "http://127.0.0.1:10809", -// Headers: map[string]string{ -// "Accept": "Application/json, text/plain, */*", -// }, -// }, "GET") -// if err != nil { -// log.Print("Request Failed: " + err.Error()) -// } -// log.Print("Status: " + strconv.Itoa(resp.Status)) -// log.Print("Body: " + resp.Body) - -// } +func TestSocks5hProxy(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("Skipping this test on non-linux platforms") + return + } + client := cycletls.Init() + resp, err := client.Do("https://ipinfo.io/json", cycletls.Options{ + Body: "", + Ja3: "771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-51-57-47-53-10,0-23-65281-10-11-35-16-5-51-43-13-45-28-21,29-23-24-25-256-257,0", + UserAgent: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0", + Proxy: "socks5h://abc:123@127.0.0.1:1087", + Headers: map[string]string{ + "Accept": "Application/json, text/plain, */*", + }, + }, "GET") + if err != nil { + t.Fatalf("Request Failed: " + err.Error()) + } + if resp.Status != 200 { + t.Fatalf("Expected %d Got %d for Status", 200, resp.Status) + } + log.Print("Body: " + resp.Body) +} \ No newline at end of file