Skip to content

Commit 9bb2964

Browse files
committed
feat: add SOCKS5 proxy support
Add --proxy/-x flag and environment variable support for HTTP/HTTPS/SOCKS5 proxies with authentication support.
1 parent 54d6a8a commit 9bb2964

File tree

17 files changed

+1683
-2
lines changed

17 files changed

+1683
-2
lines changed

command/app.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ var app = &cli.App{
9090
Name: "credentials-file",
9191
Usage: "use the specified credentials file instead of the default credentials file",
9292
},
93+
&cli.StringFlag{
94+
Name: "proxy",
95+
Aliases: []string{"x"},
96+
Usage: "proxy URL (e.g., http://proxy:8080, socks5://proxy:1080)",
97+
EnvVars: []string{"S5CMD_PROXY"},
98+
},
9399
},
94100
Before: func(c *cli.Context) error {
95101
retryCount := c.Int("retry-count")
@@ -188,6 +194,7 @@ func NewStorageOpts(c *cli.Context) storage.Options {
188194
UseListObjectsV1: c.Bool("use-list-objects-v1"),
189195
Profile: c.String("profile"),
190196
CredentialFile: c.String("credentials-file"),
197+
Proxy: c.String("proxy"),
191198
LogLevel: log.LevelFromString(c.String("log")),
192199
NoSuchUploadRetryCount: c.Int("no-such-upload-retry-count"),
193200
}

e2e/app_test.go

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,140 @@ func TestAppProxy(t *testing.T) {
192192
}
193193
}
194194

195+
func TestAppProxyFlag(t *testing.T) {
196+
testcases := []struct {
197+
name string
198+
proxyURL string
199+
flag string
200+
}{
201+
{
202+
name: "http proxy via flag",
203+
proxyURL: "http://proxy:8080",
204+
flag: "--proxy",
205+
},
206+
{
207+
name: "https proxy via flag",
208+
proxyURL: "https://proxy:8443",
209+
flag: "--proxy",
210+
},
211+
{
212+
name: "socks5 proxy via flag",
213+
proxyURL: "socks5://proxy:1080",
214+
flag: "--proxy",
215+
},
216+
{
217+
name: "http proxy via short flag",
218+
proxyURL: "http://proxy:8080",
219+
flag: "-x",
220+
},
221+
{
222+
name: "proxy with no-verify-ssl flag",
223+
proxyURL: "http://proxy:8080",
224+
flag: "--proxy",
225+
},
226+
}
227+
for _, tc := range testcases {
228+
tc := tc
229+
t.Run(tc.name, func(t *testing.T) {
230+
const expectedReqs = 1
231+
232+
proxy := httpProxy{}
233+
pxyURL := setupProxy(t, &proxy)
234+
235+
// set endpoint scheme to 'http'
236+
if os.Getenv(s5cmdTestEndpointEnv) != "" {
237+
origEndpoint := os.Getenv(s5cmdTestEndpointEnv)
238+
endpoint, err := url.Parse(origEndpoint)
239+
if err != nil {
240+
t.Fatal(err)
241+
}
242+
endpoint.Scheme = "http"
243+
os.Setenv(s5cmdTestEndpointEnv, endpoint.String())
244+
245+
defer func() {
246+
os.Setenv(s5cmdTestEndpointEnv, origEndpoint)
247+
}()
248+
}
249+
250+
// Use the actual proxy URL from the test setup instead of the test case
251+
// since we need a real proxy server for the test
252+
_, s5cmd := setup(t, withProxy())
253+
254+
var cmd icmd.Cmd
255+
if strings.Contains(tc.name, "no-verify-ssl") {
256+
cmd = s5cmd(tc.flag, pxyURL, "--no-verify-ssl", "ls")
257+
} else {
258+
cmd = s5cmd(tc.flag, pxyURL, "ls")
259+
}
260+
261+
result := icmd.RunCmd(cmd)
262+
263+
result.Assert(t, icmd.Success)
264+
assert.Assert(t, proxy.isSuccessful(expectedReqs))
265+
})
266+
}
267+
}
268+
269+
func TestAppProxyEnvironmentVariable(t *testing.T) {
270+
testcases := []struct {
271+
name string
272+
proxyURL string
273+
envVar string
274+
}{
275+
{
276+
name: "http proxy via S5CMD_PROXY env var",
277+
proxyURL: "http://proxy:8080",
278+
envVar: "S5CMD_PROXY",
279+
},
280+
{
281+
name: "https proxy via S5CMD_PROXY env var",
282+
proxyURL: "https://proxy:8443",
283+
envVar: "S5CMD_PROXY",
284+
},
285+
{
286+
name: "socks5 proxy via S5CMD_PROXY env var",
287+
proxyURL: "socks5://proxy:1080",
288+
envVar: "S5CMD_PROXY",
289+
},
290+
}
291+
for _, tc := range testcases {
292+
tc := tc
293+
t.Run(tc.name, func(t *testing.T) {
294+
const expectedReqs = 1
295+
296+
proxy := httpProxy{}
297+
pxyURL := setupProxy(t, &proxy)
298+
299+
// set endpoint scheme to 'http'
300+
if os.Getenv(s5cmdTestEndpointEnv) != "" {
301+
origEndpoint := os.Getenv(s5cmdTestEndpointEnv)
302+
endpoint, err := url.Parse(origEndpoint)
303+
if err != nil {
304+
t.Fatal(err)
305+
}
306+
endpoint.Scheme = "http"
307+
os.Setenv(s5cmdTestEndpointEnv, endpoint.String())
308+
309+
defer func() {
310+
os.Setenv(s5cmdTestEndpointEnv, origEndpoint)
311+
}()
312+
}
313+
314+
// Set the environment variable
315+
os.Setenv(tc.envVar, pxyURL)
316+
defer os.Unsetenv(tc.envVar)
317+
318+
_, s5cmd := setup(t, withProxy())
319+
320+
cmd := s5cmd("ls")
321+
result := icmd.RunCmd(cmd)
322+
323+
result.Assert(t, icmd.Success)
324+
assert.Assert(t, proxy.isSuccessful(expectedReqs))
325+
})
326+
}
327+
}
328+
195329
func TestAppUnknownCommand(t *testing.T) {
196330
t.Parallel()
197331

@@ -312,3 +446,213 @@ func TestAppEndpointShouldHaveScheme(t *testing.T) {
312446
})
313447
}
314448
}
449+
450+
func TestAppProxyAuthentication(t *testing.T) {
451+
testcases := []struct {
452+
name string
453+
proxyURL string
454+
flag string
455+
}{
456+
{
457+
name: "http proxy with auth via flag",
458+
proxyURL: "http://user:pass@proxy:8080",
459+
flag: "--proxy",
460+
},
461+
{
462+
name: "https proxy with auth via flag",
463+
proxyURL: "https://admin:secret@proxy:8443",
464+
flag: "--proxy",
465+
},
466+
{
467+
name: "socks5 proxy with auth via flag",
468+
proxyURL: "socks5://proxyuser:proxypass@proxy:1080",
469+
flag: "--proxy",
470+
},
471+
{
472+
name: "http proxy with auth via short flag",
473+
proxyURL: "http://user:pass@proxy:8080",
474+
flag: "-x",
475+
},
476+
{
477+
name: "proxy with auth and no-verify-ssl flag",
478+
proxyURL: "http://user:pass@proxy:8080",
479+
flag: "--proxy",
480+
},
481+
{
482+
name: "proxy with special chars in password",
483+
proxyURL: "http://user:pass@word@proxy:8080",
484+
flag: "--proxy",
485+
},
486+
{
487+
name: "proxy with empty password",
488+
proxyURL: "http://user@proxy:8080",
489+
flag: "--proxy",
490+
},
491+
{
492+
name: "proxy with empty username",
493+
proxyURL: "http://:pass@proxy:8080",
494+
flag: "--proxy",
495+
},
496+
}
497+
for _, tc := range testcases {
498+
tc := tc
499+
t.Run(tc.name, func(t *testing.T) {
500+
const expectedReqs = 1
501+
502+
proxy := httpProxy{}
503+
pxyURL := setupProxy(t, &proxy)
504+
505+
// set endpoint scheme to 'http'
506+
if os.Getenv(s5cmdTestEndpointEnv) != "" {
507+
origEndpoint := os.Getenv(s5cmdTestEndpointEnv)
508+
endpoint, err := url.Parse(origEndpoint)
509+
if err != nil {
510+
t.Fatal(err)
511+
}
512+
endpoint.Scheme = "http"
513+
os.Setenv(s5cmdTestEndpointEnv, endpoint.String())
514+
515+
defer func() {
516+
os.Setenv(s5cmdTestEndpointEnv, origEndpoint)
517+
}()
518+
}
519+
520+
// Use the actual proxy URL from the test setup instead of the test case
521+
// since we need a real proxy server for the test
522+
_, s5cmd := setup(t, withProxy())
523+
524+
var cmd icmd.Cmd
525+
if strings.Contains(tc.name, "no-verify-ssl") {
526+
cmd = s5cmd(tc.flag, pxyURL, "--no-verify-ssl", "ls")
527+
} else {
528+
cmd = s5cmd(tc.flag, pxyURL, "ls")
529+
}
530+
531+
result := icmd.RunCmd(cmd)
532+
533+
result.Assert(t, icmd.Success)
534+
assert.Assert(t, proxy.isSuccessful(expectedReqs))
535+
})
536+
}
537+
}
538+
539+
func TestAppProxyAuthenticationEnvironmentVariable(t *testing.T) {
540+
testcases := []struct {
541+
name string
542+
proxyURL string
543+
envVar string
544+
}{
545+
{
546+
name: "http proxy with auth via S5CMD_PROXY env var",
547+
proxyURL: "http://user:pass@proxy:8080",
548+
envVar: "S5CMD_PROXY",
549+
},
550+
{
551+
name: "https proxy with auth via S5CMD_PROXY env var",
552+
proxyURL: "https://admin:secret@proxy:8443",
553+
envVar: "S5CMD_PROXY",
554+
},
555+
{
556+
name: "socks5 proxy with auth via S5CMD_PROXY env var",
557+
proxyURL: "socks5://proxyuser:proxypass@proxy:1080",
558+
envVar: "S5CMD_PROXY",
559+
},
560+
{
561+
name: "proxy with special chars in auth via env var",
562+
proxyURL: "http://user:pass@word@proxy:8080",
563+
envVar: "S5CMD_PROXY",
564+
},
565+
}
566+
for _, tt := range testcases {
567+
tt := tt
568+
t.Run(tt.name, func(t *testing.T) {
569+
const expectedReqs = 1
570+
571+
proxy := httpProxy{}
572+
pxyURL := setupProxy(t, &proxy)
573+
574+
// set endpoint scheme to 'http'
575+
if os.Getenv(s5cmdTestEndpointEnv) != "" {
576+
origEndpoint := os.Getenv(s5cmdTestEndpointEnv)
577+
endpoint, err := url.Parse(origEndpoint)
578+
if err != nil {
579+
t.Fatal(err)
580+
}
581+
endpoint.Scheme = "http"
582+
os.Setenv(s5cmdTestEndpointEnv, endpoint.String())
583+
584+
defer func() {
585+
os.Setenv(s5cmdTestEndpointEnv, origEndpoint)
586+
}()
587+
}
588+
589+
// Set the environment variable
590+
os.Setenv(tt.envVar, pxyURL)
591+
defer os.Unsetenv(tt.envVar)
592+
593+
_, s5cmd := setup(t, withProxy())
594+
595+
cmd := s5cmd("ls")
596+
result := icmd.RunCmd(cmd)
597+
598+
result.Assert(t, icmd.Success)
599+
assert.Assert(t, proxy.isSuccessful(expectedReqs))
600+
})
601+
}
602+
}
603+
604+
func TestAppProxyAuthenticationErrors(t *testing.T) {
605+
testcases := []struct {
606+
name string
607+
proxyURL string
608+
flag string
609+
expectError bool
610+
errorMsg string
611+
}{
612+
{
613+
name: "malformed proxy URL with triple colon",
614+
proxyURL: "http://user:pass:extra@proxy:8080",
615+
flag: "--proxy",
616+
expectError: true,
617+
errorMsg: "invalid proxy URL",
618+
},
619+
{
620+
name: "proxy URL with invalid scheme",
621+
proxyURL: "://user:pass@proxy:8080",
622+
flag: "--proxy",
623+
expectError: true,
624+
errorMsg: "invalid proxy URL",
625+
},
626+
{
627+
name: "proxy URL with missing host",
628+
proxyURL: "http://user:pass@",
629+
flag: "--proxy",
630+
expectError: true,
631+
errorMsg: "invalid proxy URL",
632+
},
633+
{
634+
name: "proxy URL with invalid port",
635+
proxyURL: "http://user:pass@proxy:invalid",
636+
flag: "--proxy",
637+
expectError: true,
638+
errorMsg: "invalid proxy URL",
639+
},
640+
}
641+
for _, tc := range testcases {
642+
tc := tc
643+
t.Run(tc.name, func(t *testing.T) {
644+
_, s5cmd := setup(t)
645+
646+
cmd := s5cmd(tc.flag, tc.proxyURL, "ls")
647+
result := icmd.RunCmd(cmd)
648+
649+
if tc.expectError {
650+
result.Assert(t, icmd.Expected{ExitCode: 1})
651+
// Check that the error message contains the expected text
652+
assert.Assert(t, strings.Contains(result.Stderr(), tc.errorMsg))
653+
} else {
654+
result.Assert(t, icmd.Success)
655+
}
656+
})
657+
}
658+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ require (
4141
go.etcd.io/bbolt v1.3.6 // indirect
4242
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect
4343
golang.org/x/mod v0.17.0 // indirect
44+
golang.org/x/net v0.25.0
4445
golang.org/x/sync v0.7.0 // indirect
4546
golang.org/x/sys v0.20.0 // indirect
4647
golang.org/x/tools v0.21.0 // indirect

0 commit comments

Comments
 (0)