From 4776ba2dce7d9c3020aa70a17fe183cc43ad5932 Mon Sep 17 00:00:00 2001 From: Mons Anderson Date: Wed, 13 Aug 2025 14:32:40 +0300 Subject: [PATCH] feat: add SOCKS5 proxy support Add --proxy/-x flag and environment variable support for HTTP/HTTPS/SOCKS5 proxies with authentication support. --- command/app.go | 7 + e2e/app_test.go | 338 ++++++++++++++++++ go.mod | 1 + go.sum | 3 +- storage/s3.go | 74 +++- storage/s3_proxy_test.go | 290 +++++++++++++++ storage/storage.go | 2 + vendor/golang.org/x/net/LICENSE | 27 ++ vendor/golang.org/x/net/PATENTS | 22 ++ .../golang.org/x/net/internal/socks/client.go | 168 +++++++++ .../golang.org/x/net/internal/socks/socks.go | 317 ++++++++++++++++ vendor/golang.org/x/net/proxy/dial.go | 54 +++ vendor/golang.org/x/net/proxy/direct.go | 31 ++ vendor/golang.org/x/net/proxy/per_host.go | 155 ++++++++ vendor/golang.org/x/net/proxy/proxy.go | 149 ++++++++ vendor/golang.org/x/net/proxy/socks5.go | 42 +++ vendor/modules.txt | 4 + 17 files changed, 1682 insertions(+), 2 deletions(-) create mode 100644 storage/s3_proxy_test.go create mode 100644 vendor/golang.org/x/net/LICENSE create mode 100644 vendor/golang.org/x/net/PATENTS create mode 100644 vendor/golang.org/x/net/internal/socks/client.go create mode 100644 vendor/golang.org/x/net/internal/socks/socks.go create mode 100644 vendor/golang.org/x/net/proxy/dial.go create mode 100644 vendor/golang.org/x/net/proxy/direct.go create mode 100644 vendor/golang.org/x/net/proxy/per_host.go create mode 100644 vendor/golang.org/x/net/proxy/proxy.go create mode 100644 vendor/golang.org/x/net/proxy/socks5.go diff --git a/command/app.go b/command/app.go index f1699e43d..d4c14eb9e 100644 --- a/command/app.go +++ b/command/app.go @@ -90,6 +90,12 @@ var app = &cli.App{ Name: "credentials-file", Usage: "use the specified credentials file instead of the default credentials file", }, + &cli.StringFlag{ + Name: "proxy", + Aliases: []string{"x"}, + Usage: "proxy URL (e.g., http://proxy:8080, socks5://proxy:1080)", + EnvVars: []string{"S5CMD_PROXY"}, + }, }, Before: func(c *cli.Context) error { retryCount := c.Int("retry-count") @@ -188,6 +194,7 @@ func NewStorageOpts(c *cli.Context) storage.Options { UseListObjectsV1: c.Bool("use-list-objects-v1"), Profile: c.String("profile"), CredentialFile: c.String("credentials-file"), + Proxy: c.String("proxy"), LogLevel: log.LevelFromString(c.String("log")), NoSuchUploadRetryCount: c.Int("no-such-upload-retry-count"), } diff --git a/e2e/app_test.go b/e2e/app_test.go index 80c3d5720..ba3cb830a 100644 --- a/e2e/app_test.go +++ b/e2e/app_test.go @@ -192,6 +192,140 @@ func TestAppProxy(t *testing.T) { } } +func TestAppProxyFlag(t *testing.T) { + testcases := []struct { + name string + proxyURL string + flag string + }{ + { + name: "http proxy via flag", + proxyURL: "http://proxy:8080", + flag: "--proxy", + }, + { + name: "https proxy via flag", + proxyURL: "https://proxy:8443", + flag: "--proxy", + }, + { + name: "socks5 proxy via flag", + proxyURL: "socks5://proxy:1080", + flag: "--proxy", + }, + { + name: "http proxy via short flag", + proxyURL: "http://proxy:8080", + flag: "-x", + }, + { + name: "proxy with no-verify-ssl flag", + proxyURL: "http://proxy:8080", + flag: "--proxy", + }, + } + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + const expectedReqs = 1 + + proxy := httpProxy{} + pxyURL := setupProxy(t, &proxy) + + // set endpoint scheme to 'http' + if os.Getenv(s5cmdTestEndpointEnv) != "" { + origEndpoint := os.Getenv(s5cmdTestEndpointEnv) + endpoint, err := url.Parse(origEndpoint) + if err != nil { + t.Fatal(err) + } + endpoint.Scheme = "http" + os.Setenv(s5cmdTestEndpointEnv, endpoint.String()) + + defer func() { + os.Setenv(s5cmdTestEndpointEnv, origEndpoint) + }() + } + + // Use the actual proxy URL from the test setup instead of the test case + // since we need a real proxy server for the test + _, s5cmd := setup(t, withProxy()) + + var cmd icmd.Cmd + if strings.Contains(tc.name, "no-verify-ssl") { + cmd = s5cmd(tc.flag, pxyURL, "--no-verify-ssl", "ls") + } else { + cmd = s5cmd(tc.flag, pxyURL, "ls") + } + + result := icmd.RunCmd(cmd) + + result.Assert(t, icmd.Success) + assert.Assert(t, proxy.isSuccessful(expectedReqs)) + }) + } +} + +func TestAppProxyEnvironmentVariable(t *testing.T) { + testcases := []struct { + name string + proxyURL string + envVar string + }{ + { + name: "http proxy via S5CMD_PROXY env var", + proxyURL: "http://proxy:8080", + envVar: "S5CMD_PROXY", + }, + { + name: "https proxy via S5CMD_PROXY env var", + proxyURL: "https://proxy:8443", + envVar: "S5CMD_PROXY", + }, + { + name: "socks5 proxy via S5CMD_PROXY env var", + proxyURL: "socks5://proxy:1080", + envVar: "S5CMD_PROXY", + }, + } + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + const expectedReqs = 1 + + proxy := httpProxy{} + pxyURL := setupProxy(t, &proxy) + + // set endpoint scheme to 'http' + if os.Getenv(s5cmdTestEndpointEnv) != "" { + origEndpoint := os.Getenv(s5cmdTestEndpointEnv) + endpoint, err := url.Parse(origEndpoint) + if err != nil { + t.Fatal(err) + } + endpoint.Scheme = "http" + os.Setenv(s5cmdTestEndpointEnv, endpoint.String()) + + defer func() { + os.Setenv(s5cmdTestEndpointEnv, origEndpoint) + }() + } + + // Set the environment variable + os.Setenv(tc.envVar, pxyURL) + defer os.Unsetenv(tc.envVar) + + _, s5cmd := setup(t, withProxy()) + + cmd := s5cmd("ls") + result := icmd.RunCmd(cmd) + + result.Assert(t, icmd.Success) + assert.Assert(t, proxy.isSuccessful(expectedReqs)) + }) + } +} + func TestAppUnknownCommand(t *testing.T) { t.Parallel() @@ -312,3 +446,207 @@ func TestAppEndpointShouldHaveScheme(t *testing.T) { }) } } + +func TestAppProxyAuthentication(t *testing.T) { + testcases := []struct { + name string + proxyURL string + flag string + }{ + { + name: "http proxy with auth via flag", + proxyURL: "http://user:pass@proxy:8080", + flag: "--proxy", + }, + { + name: "https proxy with auth via flag", + proxyURL: "https://admin:secret@proxy:8443", + flag: "--proxy", + }, + { + name: "socks5 proxy with auth via flag", + proxyURL: "socks5://proxyuser:proxypass@proxy:1080", + flag: "--proxy", + }, + { + name: "http proxy with auth via short flag", + proxyURL: "http://user:pass@proxy:8080", + flag: "-x", + }, + { + name: "proxy with auth and no-verify-ssl flag", + proxyURL: "http://user:pass@proxy:8080", + flag: "--proxy", + }, + { + name: "proxy with special chars in password", + proxyURL: "http://user:pass@word@proxy:8080", + flag: "--proxy", + }, + { + name: "proxy with empty password", + proxyURL: "http://user@proxy:8080", + flag: "--proxy", + }, + { + name: "proxy with empty username", + proxyURL: "http://:pass@proxy:8080", + flag: "--proxy", + }, + } + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + const expectedReqs = 1 + + proxy := httpProxy{} + pxyURL := setupProxy(t, &proxy) + + // set endpoint scheme to 'http' + if os.Getenv(s5cmdTestEndpointEnv) != "" { + origEndpoint := os.Getenv(s5cmdTestEndpointEnv) + endpoint, err := url.Parse(origEndpoint) + if err != nil { + t.Fatal(err) + } + endpoint.Scheme = "http" + os.Setenv(s5cmdTestEndpointEnv, endpoint.String()) + + defer func() { + os.Setenv(s5cmdTestEndpointEnv, origEndpoint) + }() + } + + // Use the actual proxy URL from the test setup instead of the test case + // since we need a real proxy server for the test + _, s5cmd := setup(t, withProxy()) + + var cmd icmd.Cmd + if strings.Contains(tc.name, "no-verify-ssl") { + cmd = s5cmd(tc.flag, pxyURL, "--no-verify-ssl", "ls") + } else { + cmd = s5cmd(tc.flag, pxyURL, "ls") + } + + result := icmd.RunCmd(cmd) + + result.Assert(t, icmd.Success) + assert.Assert(t, proxy.isSuccessful(expectedReqs)) + }) + } +} + +func TestAppProxyAuthenticationEnvironmentVariable(t *testing.T) { + testcases := []struct { + name string + proxyURL string + envVar string + }{ + { + name: "http proxy with auth via S5CMD_PROXY env var", + proxyURL: "http://user:pass@proxy:8080", + envVar: "S5CMD_PROXY", + }, + { + name: "https proxy with auth via S5CMD_PROXY env var", + proxyURL: "https://admin:secret@proxy:8443", + envVar: "S5CMD_PROXY", + }, + { + name: "socks5 proxy with auth via S5CMD_PROXY env var", + proxyURL: "socks5://proxyuser:proxypass@proxy:1080", + envVar: "S5CMD_PROXY", + }, + { + name: "proxy with special chars in auth via env var", + proxyURL: "http://user:pass@word@proxy:8080", + envVar: "S5CMD_PROXY", + }, + } + for _, tt := range testcases { + tt := tt + t.Run(tt.name, func(t *testing.T) { + const expectedReqs = 1 + + proxy := httpProxy{} + pxyURL := setupProxy(t, &proxy) + + // set endpoint scheme to 'http' + if os.Getenv(s5cmdTestEndpointEnv) != "" { + origEndpoint := os.Getenv(s5cmdTestEndpointEnv) + endpoint, err := url.Parse(origEndpoint) + if err != nil { + t.Fatal(err) + } + endpoint.Scheme = "http" + os.Setenv(s5cmdTestEndpointEnv, endpoint.String()) + + defer func() { + os.Setenv(s5cmdTestEndpointEnv, origEndpoint) + }() + } + + // Set the environment variable + os.Setenv(tt.envVar, pxyURL) + defer os.Unsetenv(tt.envVar) + + _, s5cmd := setup(t, withProxy()) + + cmd := s5cmd("ls") + result := icmd.RunCmd(cmd) + + result.Assert(t, icmd.Success) + assert.Assert(t, proxy.isSuccessful(expectedReqs)) + }) + } +} + +func TestAppProxyAuthenticationErrors(t *testing.T) { + testcases := []struct { + name string + proxyURL string + flag string + expectError bool + errorMsg string + }{ + { + name: "proxy URL with invalid scheme", + proxyURL: "://user:pass@proxy:8080", + flag: "--proxy", + expectError: true, + errorMsg: "missing protocol scheme", + }, + { + name: "proxy URL with missing host", + proxyURL: "http://user:pass@", + flag: "--proxy", + expectError: true, + errorMsg: "invalid proxy URL", + }, + { + name: "proxy URL with invalid port", + proxyURL: "http://user:pass@proxy:invalid", + flag: "--proxy", + expectError: true, + errorMsg: "invalid proxy URL", + }, + } + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + _, s5cmd := setup(t) + + cmd := s5cmd(tc.flag, tc.proxyURL, "ls") + result := icmd.RunCmd(cmd) + + if tc.expectError { + result.Assert(t, icmd.Expected{ExitCode: 1}) + // Check that the error message contains the expected text + assert.Assert(t, strings.Contains(result.Stderr(), tc.errorMsg), + "Expected error message '%s' not found in stderr: %s", tc.errorMsg, result.Stderr()) + } else { + result.Assert(t, icmd.Success) + } + }) + } +} diff --git a/go.mod b/go.mod index 010dd71d9..4c2a9a1e8 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( go.etcd.io/bbolt v1.3.6 // indirect golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/tools v0.21.0 // indirect diff --git a/go.sum b/go.sum index a5a3a11f6..722080f35 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,7 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -125,8 +126,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190829051458-42f498d34c4d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/storage/s3.go b/storage/s3.go index 3313be66e..97d919bea 100644 --- a/storage/s3.go +++ b/storage/s3.go @@ -11,6 +11,7 @@ import ( "io" "math" "math/big" + "net" "net/http" urlpkg "net/url" "os" @@ -33,6 +34,7 @@ import ( "github.com/peak/s5cmd/v2/log" "github.com/peak/s5cmd/v2/storage/url" + "golang.org/x/net/proxy" ) var sentinelURL = urlpkg.URL{} @@ -90,6 +92,59 @@ func parseEndpoint(endpoint string) (urlpkg.URL, error) { return *u, nil } +// createProxyTransport creates a custom HTTP transport with SOCKS5 proxy support +func createProxyTransport(proxyURL string, noVerifySSL bool) (*http.Transport, error) { + transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + } + + // If a specific proxy URL is provided, use it + if proxyURL != "" { + proxyURLParsed, err := urlpkg.Parse(proxyURL) + if err != nil { + return nil, fmt.Errorf("invalid proxy URL %q: %v", proxyURL, err) + } else if proxyURLParsed.Host == "" { + return nil, fmt.Errorf("invalid proxy URL %q: hostname is empry", proxyURL) + } + + if proxyURLParsed.Scheme == "socks5" { + var auth *proxy.Auth + if proxyURLParsed.User != nil { + auth = &proxy.Auth{ + User: proxyURLParsed.User.Username(), + } + if pw, ok := proxyURLParsed.User.Password(); ok { + auth.Password = pw + } + } + dialer, err := proxy.SOCKS5("tcp", proxyURLParsed.Host, auth, proxy.Direct) + if err != nil { + return nil, fmt.Errorf("failed to create SOCKS5 dialer: %v", err) + } + + if dialerContext, ok := dialer.(proxy.ContextDialer); ok { + transport.DialContext = dialerContext.DialContext + } else { + // Fallback for older proxy implementations + transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialer.Dial(network, addr) + } + } + } else { + // For HTTP/HTTPS proxies, we can set the proxy function directly + transport.Proxy = func(req *http.Request) (*urlpkg.URL, error) { + return proxyURLParsed, nil + } + } + } + + if noVerifySSL { + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + + return transport, nil +} + // NewS3Storage creates new S3 session. func newS3Storage(ctx context.Context, opts Options) (*S3, error) { endpointURL, err := parseEndpoint(opts.Endpoint) @@ -1266,7 +1321,13 @@ func (sc *SessionCache) newSession(ctx context.Context, opts Options) (*session. } var httpClient *http.Client - if opts.NoVerifySSL { + if opts.Proxy != "" || opts.NoVerifySSL { + transport, err := createProxyTransport(opts.Proxy, opts.NoVerifySSL) + if err != nil { + return nil, err + } + httpClient = &http.Client{Transport: transport} + } else if opts.NoVerifySSL { httpClient = insecureHTTPClient } awsCfg = awsCfg. @@ -1388,6 +1449,17 @@ func newCustomRetryer(maxRetries int) *customRetryer { // ShouldRetry overrides SDK's built in DefaultRetryer, adding custom retry // logics that are not included in the SDK. func (c *customRetryer) ShouldRetry(req *request.Request) bool { + // Don't retry proxy connection errors - they won't get better + if req.Error != nil { + errMsg := req.Error.Error() + if strings.Contains(errMsg, "connection refused") || + strings.Contains(errMsg, "failed to connect to SOCKS5 proxy") || + strings.Contains(errMsg, "no route to host") || + strings.Contains(errMsg, "network is unreachable") { + return false + } + } + shouldRetry := errHasCode(req.Error, "InternalError") || errHasCode(req.Error, "RequestTimeTooSkewed") || errHasCode(req.Error, "SlowDown") || strings.Contains(req.Error.Error(), "connection reset") || strings.Contains(req.Error.Error(), "connection timed out") if !shouldRetry { shouldRetry = c.DefaultRetryer.ShouldRetry(req) diff --git a/storage/s3_proxy_test.go b/storage/s3_proxy_test.go new file mode 100644 index 000000000..397fe6976 --- /dev/null +++ b/storage/s3_proxy_test.go @@ -0,0 +1,290 @@ +package storage + +import ( + "net/http" + "net/url" + "strings" + "testing" + + "github.com/peak/s5cmd/v2/log" + "gotest.tools/v3/assert" +) + +func TestCreateProxyTransport(t *testing.T) { + // Initialize log system for tests + log.Init("error", false) + + tests := []struct { + name string + proxyURL string + noVerifySSL bool + expectError bool + }{ + { + name: "empty proxy URL", + proxyURL: "", + noVerifySSL: false, + expectError: false, + }, + { + name: "http proxy URL", + proxyURL: "http://proxy:8080", + noVerifySSL: false, + expectError: false, + }, + { + name: "https proxy URL", + proxyURL: "https://proxy:8443", + noVerifySSL: false, + expectError: false, + }, + { + name: "socks5 proxy URL", + proxyURL: "socks5://proxy:1080", + noVerifySSL: false, + expectError: false, + }, + { + name: "invalid proxy URL", + proxyURL: "://invalid", + noVerifySSL: false, + expectError: true, + }, + { + name: "proxy with no verify SSL", + proxyURL: "http://proxy:8080", + noVerifySSL: true, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + transport, err := createProxyTransport(tt.proxyURL, tt.noVerifySSL) + + if tt.expectError { + assert.Assert(t, err != nil, "expected error for invalid proxy URL") + return + } + + assert.NilError(t, err) + assert.Assert(t, transport != nil) + + // Test that the transport has the expected configuration + if tt.noVerifySSL { + assert.Assert(t, transport.TLSClientConfig != nil) + assert.Assert(t, transport.TLSClientConfig.InsecureSkipVerify) + } + + // Test proxy function for HTTP/HTTPS proxies + if tt.proxyURL != "" && tt.proxyURL != "socks5://proxy:1080" { + req, _ := http.NewRequest("GET", "http://example.com", nil) + proxyURL, err := transport.Proxy(req) + assert.NilError(t, err) + assert.Assert(t, proxyURL != nil) + } + }) + } +} + +func TestProxyURLParsing(t *testing.T) { + tests := []struct { + name string + proxyURL string + expected *url.URL + }{ + { + name: "http proxy", + proxyURL: "http://proxy:8080", + expected: &url.URL{Scheme: "http", Host: "proxy:8080"}, + }, + { + name: "https proxy", + proxyURL: "https://proxy:8443", + expected: &url.URL{Scheme: "https", Host: "proxy:8443"}, + }, + { + name: "socks5 proxy", + proxyURL: "socks5://proxy:1080", + expected: &url.URL{Scheme: "socks5", Host: "proxy:1080"}, + }, + { + name: "proxy with path", + proxyURL: "http://proxy:8080/path", + expected: &url.URL{Scheme: "http", Host: "proxy:8080", Path: "/path"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parsed, err := url.Parse(tt.proxyURL) + assert.NilError(t, err) + assert.Equal(t, tt.expected.Scheme, parsed.Scheme) + assert.Equal(t, tt.expected.Host, parsed.Host) + if tt.expected.Path != "" { + assert.Equal(t, tt.expected.Path, parsed.Path) + } + }) + } +} + +func TestProxyAuthenticationParsing(t *testing.T) { + tests := []struct { + name string + proxyURL string + expectedUser string + expectedPass string + expectedHost string + expectError bool + }{ + { + name: "http proxy with auth", + proxyURL: "http://user:pass@proxy:8080", + expectedUser: "user", + expectedPass: "pass", + expectedHost: "proxy:8080", + expectError: false, + }, + { + name: "https proxy with auth", + proxyURL: "https://admin:secret@proxy:8443", + expectedUser: "admin", + expectedPass: "secret", + expectedHost: "proxy:8443", + expectError: false, + }, + { + name: "socks5 proxy with auth", + proxyURL: "socks5://proxyuser:proxypass@proxy:1080", + expectedUser: "proxyuser", + expectedPass: "proxypass", + expectedHost: "proxy:1080", + expectError: false, + }, + { + name: "proxy with special chars in password", + proxyURL: "http://user:pass@word@proxy:8080", + expectedUser: "user", + expectedPass: "pass@word", + expectedHost: "proxy:8080", + expectError: false, + }, + { + name: "proxy with empty password", + proxyURL: "http://user@proxy:8080", + expectedUser: "user", + expectedPass: "", + expectedHost: "proxy:8080", + expectError: false, + }, + { + name: "proxy with empty username", + proxyURL: "http://:pass@proxy:8080", + expectedUser: "", + expectedPass: "pass", + expectedHost: "proxy:8080", + expectError: false, + }, + { + name: "invalid proxy URL with malformed auth", + proxyURL: "://user:pass@proxy:8080", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parsed, err := url.Parse(tt.proxyURL) + + if tt.expectError { + assert.Assert(t, err != nil, "expected error for malformed proxy URL") + return + } + + assert.NilError(t, err) + assert.Equal(t, tt.expectedUser, parsed.User.Username()) + + if tt.expectedPass != "" { + pass, _ := parsed.User.Password() + assert.Equal(t, tt.expectedPass, pass) + } + + assert.Equal(t, tt.expectedHost, parsed.Host) + }) + } +} + +func TestProxyAuthenticationTransport(t *testing.T) { + tests := []struct { + name string + proxyURL string + noVerifySSL bool + expectError bool + }{ + { + name: "http proxy with auth", + proxyURL: "http://user:pass@proxy:8080", + noVerifySSL: false, + expectError: false, + }, + { + name: "https proxy with auth", + proxyURL: "https://admin:secret@proxy:8443", + noVerifySSL: false, + expectError: false, + }, + { + name: "socks5 proxy with auth", + proxyURL: "socks5://proxyuser:proxypass@proxy:1080", + noVerifySSL: false, + expectError: false, + }, + { + name: "proxy with auth and no verify SSL", + proxyURL: "http://user:pass@proxy:8080", + noVerifySSL: true, + expectError: false, + }, + { + name: "proxy with special chars in auth", + proxyURL: "http://user:pass@word@proxy:8080", + noVerifySSL: false, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + transport, err := createProxyTransport(tt.proxyURL, tt.noVerifySSL) + + if tt.expectError { + assert.Error(t, err, "expected error for proxy transport creation") + return + } + + assert.NilError(t, err) + assert.Assert(t, transport != nil) + + // Test that the transport has the expected configuration + if tt.noVerifySSL { + assert.Assert(t, transport.TLSClientConfig != nil) + assert.Assert(t, transport.TLSClientConfig.InsecureSkipVerify) + } + + // Test proxy function for HTTP/HTTPS proxies + if strings.Contains(tt.proxyURL, "http") { + req, _ := http.NewRequest("GET", "http://example.com", nil) + proxyURL, err := transport.Proxy(req) + assert.NilError(t, err) + assert.Assert(t, proxyURL != nil) + + // Verify that credentials are preserved in the proxy URL + if strings.Contains(tt.proxyURL, "@") { + assert.Assert(t, proxyURL.User != nil) + username := proxyURL.User.Username() + assert.Assert(t, username != "") + } + } + }) + } +} diff --git a/storage/storage.go b/storage/storage.go index 1a6171263..50fe0a3d4 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -69,6 +69,7 @@ func NewRemoteClient(ctx context.Context, url *url.URL, opts Options) (*S3, erro Profile: opts.Profile, CredentialFile: opts.CredentialFile, LogLevel: opts.LogLevel, + Proxy: opts.Proxy, bucket: url.Bucket, region: opts.region, } @@ -95,6 +96,7 @@ type Options struct { RequestPayer string Profile string CredentialFile string + Proxy string bucket string region string } diff --git a/vendor/golang.org/x/net/LICENSE b/vendor/golang.org/x/net/LICENSE new file mode 100644 index 000000000..6a66aea5e --- /dev/null +++ b/vendor/golang.org/x/net/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/net/PATENTS b/vendor/golang.org/x/net/PATENTS new file mode 100644 index 000000000..733099041 --- /dev/null +++ b/vendor/golang.org/x/net/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/net/internal/socks/client.go b/vendor/golang.org/x/net/internal/socks/client.go new file mode 100644 index 000000000..3d6f516a5 --- /dev/null +++ b/vendor/golang.org/x/net/internal/socks/client.go @@ -0,0 +1,168 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package socks + +import ( + "context" + "errors" + "io" + "net" + "strconv" + "time" +) + +var ( + noDeadline = time.Time{} + aLongTimeAgo = time.Unix(1, 0) +) + +func (d *Dialer) connect(ctx context.Context, c net.Conn, address string) (_ net.Addr, ctxErr error) { + host, port, err := splitHostPort(address) + if err != nil { + return nil, err + } + if deadline, ok := ctx.Deadline(); ok && !deadline.IsZero() { + c.SetDeadline(deadline) + defer c.SetDeadline(noDeadline) + } + if ctx != context.Background() { + errCh := make(chan error, 1) + done := make(chan struct{}) + defer func() { + close(done) + if ctxErr == nil { + ctxErr = <-errCh + } + }() + go func() { + select { + case <-ctx.Done(): + c.SetDeadline(aLongTimeAgo) + errCh <- ctx.Err() + case <-done: + errCh <- nil + } + }() + } + + b := make([]byte, 0, 6+len(host)) // the size here is just an estimate + b = append(b, Version5) + if len(d.AuthMethods) == 0 || d.Authenticate == nil { + b = append(b, 1, byte(AuthMethodNotRequired)) + } else { + ams := d.AuthMethods + if len(ams) > 255 { + return nil, errors.New("too many authentication methods") + } + b = append(b, byte(len(ams))) + for _, am := range ams { + b = append(b, byte(am)) + } + } + if _, ctxErr = c.Write(b); ctxErr != nil { + return + } + + if _, ctxErr = io.ReadFull(c, b[:2]); ctxErr != nil { + return + } + if b[0] != Version5 { + return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0]))) + } + am := AuthMethod(b[1]) + if am == AuthMethodNoAcceptableMethods { + return nil, errors.New("no acceptable authentication methods") + } + if d.Authenticate != nil { + if ctxErr = d.Authenticate(ctx, c, am); ctxErr != nil { + return + } + } + + b = b[:0] + b = append(b, Version5, byte(d.cmd), 0) + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + b = append(b, AddrTypeIPv4) + b = append(b, ip4...) + } else if ip6 := ip.To16(); ip6 != nil { + b = append(b, AddrTypeIPv6) + b = append(b, ip6...) + } else { + return nil, errors.New("unknown address type") + } + } else { + if len(host) > 255 { + return nil, errors.New("FQDN too long") + } + b = append(b, AddrTypeFQDN) + b = append(b, byte(len(host))) + b = append(b, host...) + } + b = append(b, byte(port>>8), byte(port)) + if _, ctxErr = c.Write(b); ctxErr != nil { + return + } + + if _, ctxErr = io.ReadFull(c, b[:4]); ctxErr != nil { + return + } + if b[0] != Version5 { + return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0]))) + } + if cmdErr := Reply(b[1]); cmdErr != StatusSucceeded { + return nil, errors.New("unknown error " + cmdErr.String()) + } + if b[2] != 0 { + return nil, errors.New("non-zero reserved field") + } + l := 2 + var a Addr + switch b[3] { + case AddrTypeIPv4: + l += net.IPv4len + a.IP = make(net.IP, net.IPv4len) + case AddrTypeIPv6: + l += net.IPv6len + a.IP = make(net.IP, net.IPv6len) + case AddrTypeFQDN: + if _, err := io.ReadFull(c, b[:1]); err != nil { + return nil, err + } + l += int(b[0]) + default: + return nil, errors.New("unknown address type " + strconv.Itoa(int(b[3]))) + } + if cap(b) < l { + b = make([]byte, l) + } else { + b = b[:l] + } + if _, ctxErr = io.ReadFull(c, b); ctxErr != nil { + return + } + if a.IP != nil { + copy(a.IP, b) + } else { + a.Name = string(b[:len(b)-2]) + } + a.Port = int(b[len(b)-2])<<8 | int(b[len(b)-1]) + return &a, nil +} + +func splitHostPort(address string) (string, int, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return "", 0, err + } + portnum, err := strconv.Atoi(port) + if err != nil { + return "", 0, err + } + if 1 > portnum || portnum > 0xffff { + return "", 0, errors.New("port number out of range " + port) + } + return host, portnum, nil +} diff --git a/vendor/golang.org/x/net/internal/socks/socks.go b/vendor/golang.org/x/net/internal/socks/socks.go new file mode 100644 index 000000000..84fcc32b6 --- /dev/null +++ b/vendor/golang.org/x/net/internal/socks/socks.go @@ -0,0 +1,317 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package socks provides a SOCKS version 5 client implementation. +// +// SOCKS protocol version 5 is defined in RFC 1928. +// Username/Password authentication for SOCKS version 5 is defined in +// RFC 1929. +package socks + +import ( + "context" + "errors" + "io" + "net" + "strconv" +) + +// A Command represents a SOCKS command. +type Command int + +func (cmd Command) String() string { + switch cmd { + case CmdConnect: + return "socks connect" + case cmdBind: + return "socks bind" + default: + return "socks " + strconv.Itoa(int(cmd)) + } +} + +// An AuthMethod represents a SOCKS authentication method. +type AuthMethod int + +// A Reply represents a SOCKS command reply code. +type Reply int + +func (code Reply) String() string { + switch code { + case StatusSucceeded: + return "succeeded" + case 0x01: + return "general SOCKS server failure" + case 0x02: + return "connection not allowed by ruleset" + case 0x03: + return "network unreachable" + case 0x04: + return "host unreachable" + case 0x05: + return "connection refused" + case 0x06: + return "TTL expired" + case 0x07: + return "command not supported" + case 0x08: + return "address type not supported" + default: + return "unknown code: " + strconv.Itoa(int(code)) + } +} + +// Wire protocol constants. +const ( + Version5 = 0x05 + + AddrTypeIPv4 = 0x01 + AddrTypeFQDN = 0x03 + AddrTypeIPv6 = 0x04 + + CmdConnect Command = 0x01 // establishes an active-open forward proxy connection + cmdBind Command = 0x02 // establishes a passive-open forward proxy connection + + AuthMethodNotRequired AuthMethod = 0x00 // no authentication required + AuthMethodUsernamePassword AuthMethod = 0x02 // use username/password + AuthMethodNoAcceptableMethods AuthMethod = 0xff // no acceptable authentication methods + + StatusSucceeded Reply = 0x00 +) + +// An Addr represents a SOCKS-specific address. +// Either Name or IP is used exclusively. +type Addr struct { + Name string // fully-qualified domain name + IP net.IP + Port int +} + +func (a *Addr) Network() string { return "socks" } + +func (a *Addr) String() string { + if a == nil { + return "" + } + port := strconv.Itoa(a.Port) + if a.IP == nil { + return net.JoinHostPort(a.Name, port) + } + return net.JoinHostPort(a.IP.String(), port) +} + +// A Conn represents a forward proxy connection. +type Conn struct { + net.Conn + + boundAddr net.Addr +} + +// BoundAddr returns the address assigned by the proxy server for +// connecting to the command target address from the proxy server. +func (c *Conn) BoundAddr() net.Addr { + if c == nil { + return nil + } + return c.boundAddr +} + +// A Dialer holds SOCKS-specific options. +type Dialer struct { + cmd Command // either CmdConnect or cmdBind + proxyNetwork string // network between a proxy server and a client + proxyAddress string // proxy server address + + // ProxyDial specifies the optional dial function for + // establishing the transport connection. + ProxyDial func(context.Context, string, string) (net.Conn, error) + + // AuthMethods specifies the list of request authentication + // methods. + // If empty, SOCKS client requests only AuthMethodNotRequired. + AuthMethods []AuthMethod + + // Authenticate specifies the optional authentication + // function. It must be non-nil when AuthMethods is not empty. + // It must return an error when the authentication is failed. + Authenticate func(context.Context, io.ReadWriter, AuthMethod) error +} + +// DialContext connects to the provided address on the provided +// network. +// +// The returned error value may be a net.OpError. When the Op field of +// net.OpError contains "socks", the Source field contains a proxy +// server address and the Addr field contains a command target +// address. +// +// See func Dial of the net package of standard library for a +// description of the network and address parameters. +func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + if err := d.validateTarget(network, address); err != nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} + } + if ctx == nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: errors.New("nil context")} + } + var err error + var c net.Conn + if d.ProxyDial != nil { + c, err = d.ProxyDial(ctx, d.proxyNetwork, d.proxyAddress) + } else { + var dd net.Dialer + c, err = dd.DialContext(ctx, d.proxyNetwork, d.proxyAddress) + } + if err != nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} + } + a, err := d.connect(ctx, c, address) + if err != nil { + c.Close() + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} + } + return &Conn{Conn: c, boundAddr: a}, nil +} + +// DialWithConn initiates a connection from SOCKS server to the target +// network and address using the connection c that is already +// connected to the SOCKS server. +// +// It returns the connection's local address assigned by the SOCKS +// server. +func (d *Dialer) DialWithConn(ctx context.Context, c net.Conn, network, address string) (net.Addr, error) { + if err := d.validateTarget(network, address); err != nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} + } + if ctx == nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: errors.New("nil context")} + } + a, err := d.connect(ctx, c, address) + if err != nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} + } + return a, nil +} + +// Dial connects to the provided address on the provided network. +// +// Unlike DialContext, it returns a raw transport connection instead +// of a forward proxy connection. +// +// Deprecated: Use DialContext or DialWithConn instead. +func (d *Dialer) Dial(network, address string) (net.Conn, error) { + if err := d.validateTarget(network, address); err != nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} + } + var err error + var c net.Conn + if d.ProxyDial != nil { + c, err = d.ProxyDial(context.Background(), d.proxyNetwork, d.proxyAddress) + } else { + c, err = net.Dial(d.proxyNetwork, d.proxyAddress) + } + if err != nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} + } + if _, err := d.DialWithConn(context.Background(), c, network, address); err != nil { + c.Close() + return nil, err + } + return c, nil +} + +func (d *Dialer) validateTarget(network, address string) error { + switch network { + case "tcp", "tcp6", "tcp4": + default: + return errors.New("network not implemented") + } + switch d.cmd { + case CmdConnect, cmdBind: + default: + return errors.New("command not implemented") + } + return nil +} + +func (d *Dialer) pathAddrs(address string) (proxy, dst net.Addr, err error) { + for i, s := range []string{d.proxyAddress, address} { + host, port, err := splitHostPort(s) + if err != nil { + return nil, nil, err + } + a := &Addr{Port: port} + a.IP = net.ParseIP(host) + if a.IP == nil { + a.Name = host + } + if i == 0 { + proxy = a + } else { + dst = a + } + } + return +} + +// NewDialer returns a new Dialer that dials through the provided +// proxy server's network and address. +func NewDialer(network, address string) *Dialer { + return &Dialer{proxyNetwork: network, proxyAddress: address, cmd: CmdConnect} +} + +const ( + authUsernamePasswordVersion = 0x01 + authStatusSucceeded = 0x00 +) + +// UsernamePassword are the credentials for the username/password +// authentication method. +type UsernamePassword struct { + Username string + Password string +} + +// Authenticate authenticates a pair of username and password with the +// proxy server. +func (up *UsernamePassword) Authenticate(ctx context.Context, rw io.ReadWriter, auth AuthMethod) error { + switch auth { + case AuthMethodNotRequired: + return nil + case AuthMethodUsernamePassword: + if len(up.Username) == 0 || len(up.Username) > 255 || len(up.Password) > 255 { + return errors.New("invalid username/password") + } + b := []byte{authUsernamePasswordVersion} + b = append(b, byte(len(up.Username))) + b = append(b, up.Username...) + b = append(b, byte(len(up.Password))) + b = append(b, up.Password...) + // TODO(mikio): handle IO deadlines and cancelation if + // necessary + if _, err := rw.Write(b); err != nil { + return err + } + if _, err := io.ReadFull(rw, b[:2]); err != nil { + return err + } + if b[0] != authUsernamePasswordVersion { + return errors.New("invalid username/password version") + } + if b[1] != authStatusSucceeded { + return errors.New("username/password authentication failed") + } + return nil + } + return errors.New("unsupported authentication method " + strconv.Itoa(int(auth))) +} diff --git a/vendor/golang.org/x/net/proxy/dial.go b/vendor/golang.org/x/net/proxy/dial.go new file mode 100644 index 000000000..811c2e4e9 --- /dev/null +++ b/vendor/golang.org/x/net/proxy/dial.go @@ -0,0 +1,54 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package proxy + +import ( + "context" + "net" +) + +// A ContextDialer dials using a context. +type ContextDialer interface { + DialContext(ctx context.Context, network, address string) (net.Conn, error) +} + +// Dial works like DialContext on net.Dialer but using a dialer returned by FromEnvironment. +// +// The passed ctx is only used for returning the Conn, not the lifetime of the Conn. +// +// Custom dialers (registered via RegisterDialerType) that do not implement ContextDialer +// can leak a goroutine for as long as it takes the underlying Dialer implementation to timeout. +// +// A Conn returned from a successful Dial after the context has been cancelled will be immediately closed. +func Dial(ctx context.Context, network, address string) (net.Conn, error) { + d := FromEnvironment() + if xd, ok := d.(ContextDialer); ok { + return xd.DialContext(ctx, network, address) + } + return dialContext(ctx, d, network, address) +} + +// WARNING: this can leak a goroutine for as long as the underlying Dialer implementation takes to timeout +// A Conn returned from a successful Dial after the context has been cancelled will be immediately closed. +func dialContext(ctx context.Context, d Dialer, network, address string) (net.Conn, error) { + var ( + conn net.Conn + done = make(chan struct{}, 1) + err error + ) + go func() { + conn, err = d.Dial(network, address) + close(done) + if conn != nil && ctx.Err() != nil { + conn.Close() + } + }() + select { + case <-ctx.Done(): + err = ctx.Err() + case <-done: + } + return conn, err +} diff --git a/vendor/golang.org/x/net/proxy/direct.go b/vendor/golang.org/x/net/proxy/direct.go new file mode 100644 index 000000000..3d66bdef9 --- /dev/null +++ b/vendor/golang.org/x/net/proxy/direct.go @@ -0,0 +1,31 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package proxy + +import ( + "context" + "net" +) + +type direct struct{} + +// Direct implements Dialer by making network connections directly using net.Dial or net.DialContext. +var Direct = direct{} + +var ( + _ Dialer = Direct + _ ContextDialer = Direct +) + +// Dial directly invokes net.Dial with the supplied parameters. +func (direct) Dial(network, addr string) (net.Conn, error) { + return net.Dial(network, addr) +} + +// DialContext instantiates a net.Dialer and invokes its DialContext receiver with the supplied parameters. +func (direct) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, network, addr) +} diff --git a/vendor/golang.org/x/net/proxy/per_host.go b/vendor/golang.org/x/net/proxy/per_host.go new file mode 100644 index 000000000..573fe79e8 --- /dev/null +++ b/vendor/golang.org/x/net/proxy/per_host.go @@ -0,0 +1,155 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package proxy + +import ( + "context" + "net" + "strings" +) + +// A PerHost directs connections to a default Dialer unless the host name +// requested matches one of a number of exceptions. +type PerHost struct { + def, bypass Dialer + + bypassNetworks []*net.IPNet + bypassIPs []net.IP + bypassZones []string + bypassHosts []string +} + +// NewPerHost returns a PerHost Dialer that directs connections to either +// defaultDialer or bypass, depending on whether the connection matches one of +// the configured rules. +func NewPerHost(defaultDialer, bypass Dialer) *PerHost { + return &PerHost{ + def: defaultDialer, + bypass: bypass, + } +} + +// Dial connects to the address addr on the given network through either +// defaultDialer or bypass. +func (p *PerHost) Dial(network, addr string) (c net.Conn, err error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + return p.dialerForRequest(host).Dial(network, addr) +} + +// DialContext connects to the address addr on the given network through either +// defaultDialer or bypass. +func (p *PerHost) DialContext(ctx context.Context, network, addr string) (c net.Conn, err error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + d := p.dialerForRequest(host) + if x, ok := d.(ContextDialer); ok { + return x.DialContext(ctx, network, addr) + } + return dialContext(ctx, d, network, addr) +} + +func (p *PerHost) dialerForRequest(host string) Dialer { + if ip := net.ParseIP(host); ip != nil { + for _, net := range p.bypassNetworks { + if net.Contains(ip) { + return p.bypass + } + } + for _, bypassIP := range p.bypassIPs { + if bypassIP.Equal(ip) { + return p.bypass + } + } + return p.def + } + + for _, zone := range p.bypassZones { + if strings.HasSuffix(host, zone) { + return p.bypass + } + if host == zone[1:] { + // For a zone ".example.com", we match "example.com" + // too. + return p.bypass + } + } + for _, bypassHost := range p.bypassHosts { + if bypassHost == host { + return p.bypass + } + } + return p.def +} + +// AddFromString parses a string that contains comma-separated values +// specifying hosts that should use the bypass proxy. Each value is either an +// IP address, a CIDR range, a zone (*.example.com) or a host name +// (localhost). A best effort is made to parse the string and errors are +// ignored. +func (p *PerHost) AddFromString(s string) { + hosts := strings.Split(s, ",") + for _, host := range hosts { + host = strings.TrimSpace(host) + if len(host) == 0 { + continue + } + if strings.Contains(host, "/") { + // We assume that it's a CIDR address like 127.0.0.0/8 + if _, net, err := net.ParseCIDR(host); err == nil { + p.AddNetwork(net) + } + continue + } + if ip := net.ParseIP(host); ip != nil { + p.AddIP(ip) + continue + } + if strings.HasPrefix(host, "*.") { + p.AddZone(host[1:]) + continue + } + p.AddHost(host) + } +} + +// AddIP specifies an IP address that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match an IP. +func (p *PerHost) AddIP(ip net.IP) { + p.bypassIPs = append(p.bypassIPs, ip) +} + +// AddNetwork specifies an IP range that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match. +func (p *PerHost) AddNetwork(net *net.IPNet) { + p.bypassNetworks = append(p.bypassNetworks, net) +} + +// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of +// "example.com" matches "example.com" and all of its subdomains. +func (p *PerHost) AddZone(zone string) { + if strings.HasSuffix(zone, ".") { + zone = zone[:len(zone)-1] + } + if !strings.HasPrefix(zone, ".") { + zone = "." + zone + } + p.bypassZones = append(p.bypassZones, zone) +} + +// AddHost specifies a host name that will use the bypass proxy. +func (p *PerHost) AddHost(host string) { + if strings.HasSuffix(host, ".") { + host = host[:len(host)-1] + } + p.bypassHosts = append(p.bypassHosts, host) +} diff --git a/vendor/golang.org/x/net/proxy/proxy.go b/vendor/golang.org/x/net/proxy/proxy.go new file mode 100644 index 000000000..9ff4b9a77 --- /dev/null +++ b/vendor/golang.org/x/net/proxy/proxy.go @@ -0,0 +1,149 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package proxy provides support for a variety of protocols to proxy network +// data. +package proxy // import "golang.org/x/net/proxy" + +import ( + "errors" + "net" + "net/url" + "os" + "sync" +) + +// A Dialer is a means to establish a connection. +// Custom dialers should also implement ContextDialer. +type Dialer interface { + // Dial connects to the given address via the proxy. + Dial(network, addr string) (c net.Conn, err error) +} + +// Auth contains authentication parameters that specific Dialers may require. +type Auth struct { + User, Password string +} + +// FromEnvironment returns the dialer specified by the proxy-related +// variables in the environment and makes underlying connections +// directly. +func FromEnvironment() Dialer { + return FromEnvironmentUsing(Direct) +} + +// FromEnvironmentUsing returns the dialer specify by the proxy-related +// variables in the environment and makes underlying connections +// using the provided forwarding Dialer (for instance, a *net.Dialer +// with desired configuration). +func FromEnvironmentUsing(forward Dialer) Dialer { + allProxy := allProxyEnv.Get() + if len(allProxy) == 0 { + return forward + } + + proxyURL, err := url.Parse(allProxy) + if err != nil { + return forward + } + proxy, err := FromURL(proxyURL, forward) + if err != nil { + return forward + } + + noProxy := noProxyEnv.Get() + if len(noProxy) == 0 { + return proxy + } + + perHost := NewPerHost(proxy, forward) + perHost.AddFromString(noProxy) + return perHost +} + +// proxySchemes is a map from URL schemes to a function that creates a Dialer +// from a URL with such a scheme. +var proxySchemes map[string]func(*url.URL, Dialer) (Dialer, error) + +// RegisterDialerType takes a URL scheme and a function to generate Dialers from +// a URL with that scheme and a forwarding Dialer. Registered schemes are used +// by FromURL. +func RegisterDialerType(scheme string, f func(*url.URL, Dialer) (Dialer, error)) { + if proxySchemes == nil { + proxySchemes = make(map[string]func(*url.URL, Dialer) (Dialer, error)) + } + proxySchemes[scheme] = f +} + +// FromURL returns a Dialer given a URL specification and an underlying +// Dialer for it to make network requests. +func FromURL(u *url.URL, forward Dialer) (Dialer, error) { + var auth *Auth + if u.User != nil { + auth = new(Auth) + auth.User = u.User.Username() + if p, ok := u.User.Password(); ok { + auth.Password = p + } + } + + switch u.Scheme { + case "socks5", "socks5h": + addr := u.Hostname() + port := u.Port() + if port == "" { + port = "1080" + } + return SOCKS5("tcp", net.JoinHostPort(addr, port), auth, forward) + } + + // If the scheme doesn't match any of the built-in schemes, see if it + // was registered by another package. + if proxySchemes != nil { + if f, ok := proxySchemes[u.Scheme]; ok { + return f(u, forward) + } + } + + return nil, errors.New("proxy: unknown scheme: " + u.Scheme) +} + +var ( + allProxyEnv = &envOnce{ + names: []string{"ALL_PROXY", "all_proxy"}, + } + noProxyEnv = &envOnce{ + names: []string{"NO_PROXY", "no_proxy"}, + } +) + +// envOnce looks up an environment variable (optionally by multiple +// names) once. It mitigates expensive lookups on some platforms +// (e.g. Windows). +// (Borrowed from net/http/transport.go) +type envOnce struct { + names []string + once sync.Once + val string +} + +func (e *envOnce) Get() string { + e.once.Do(e.init) + return e.val +} + +func (e *envOnce) init() { + for _, n := range e.names { + e.val = os.Getenv(n) + if e.val != "" { + return + } + } +} + +// reset is used by tests +func (e *envOnce) reset() { + e.once = sync.Once{} + e.val = "" +} diff --git a/vendor/golang.org/x/net/proxy/socks5.go b/vendor/golang.org/x/net/proxy/socks5.go new file mode 100644 index 000000000..c91651f96 --- /dev/null +++ b/vendor/golang.org/x/net/proxy/socks5.go @@ -0,0 +1,42 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package proxy + +import ( + "context" + "net" + + "golang.org/x/net/internal/socks" +) + +// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given +// address with an optional username and password. +// See RFC 1928 and RFC 1929. +func SOCKS5(network, address string, auth *Auth, forward Dialer) (Dialer, error) { + d := socks.NewDialer(network, address) + if forward != nil { + if f, ok := forward.(ContextDialer); ok { + d.ProxyDial = func(ctx context.Context, network string, address string) (net.Conn, error) { + return f.DialContext(ctx, network, address) + } + } else { + d.ProxyDial = func(ctx context.Context, network string, address string) (net.Conn, error) { + return dialContext(ctx, forward, network, address) + } + } + } + if auth != nil { + up := socks.UsernamePassword{ + Username: auth.User, + Password: auth.Password, + } + d.AuthMethods = []socks.AuthMethod{ + socks.AuthMethodNotRequired, + socks.AuthMethodUsernamePassword, + } + d.Authenticate = up.Authenticate + } + return d, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 139ece3ab..507d3e10d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -164,6 +164,10 @@ golang.org/x/mod/internal/lazyregexp golang.org/x/mod/modfile golang.org/x/mod/module golang.org/x/mod/semver +# golang.org/x/net v0.25.0 +## explicit; go 1.18 +golang.org/x/net/internal/socks +golang.org/x/net/proxy # golang.org/x/sync v0.7.0 ## explicit; go 1.18 golang.org/x/sync/errgroup