From 2e64c393914a2aadf41a87c4284fd1cf6283a47f Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Fri, 2 Jan 2026 04:26:58 +0000 Subject: [PATCH 1/9] feat: support ALL_PROXY environment variable (#69) --- cmd/claws/main.go | 33 ++++++++- cmd/claws/proxy_test.go | 147 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 cmd/claws/proxy_test.go diff --git a/cmd/claws/main.go b/cmd/claws/main.go index 4724a22c..fb251203 100644 --- a/cmd/claws/main.go +++ b/cmd/claws/main.go @@ -19,7 +19,8 @@ import ( var version = "dev" func main() { - // Parse command line flags + propagateAllProxy() + opts := parseFlags() // Apply CLI options to global config @@ -167,4 +168,34 @@ func printUsage() { fmt.Println() fmt.Println("Environment Variables:") fmt.Println(" CLAWS_READ_ONLY=1|true Enable read-only mode") + fmt.Println(" ALL_PROXY Propagated to HTTPS_PROXY if not set") +} + +// getEnvWithFallback returns the first non-empty value from the given env var names. +func getEnvWithFallback(names ...string) string { + for _, name := range names { + if v := os.Getenv(name); v != "" { + return v + } + } + return "" +} + +// propagateAllProxy copies ALL_PROXY to HTTPS_PROXY if HTTPS_PROXY is not set. +// Go's net/http ignores ALL_PROXY and only reads HTTP_PROXY/HTTPS_PROXY. +// AWS APIs use HTTPS, so we only need HTTPS_PROXY. +// HTTP is only used for IMDS (169.254.169.254) which must NOT be proxied. +func propagateAllProxy() { + allProxy := getEnvWithFallback("ALL_PROXY", "all_proxy") + if allProxy == "" { + return + } + if getEnvWithFallback("HTTPS_PROXY", "https_proxy") != "" { + return + } + if err := os.Setenv("HTTPS_PROXY", allProxy); err != nil { + log.Warn("failed to set HTTPS_PROXY", "error", err) + return + } + log.Info("propagated ALL_PROXY to HTTPS_PROXY", "proxy", allProxy) } diff --git a/cmd/claws/proxy_test.go b/cmd/claws/proxy_test.go new file mode 100644 index 00000000..f84e61ba --- /dev/null +++ b/cmd/claws/proxy_test.go @@ -0,0 +1,147 @@ +package main + +import ( + "os" + "testing" +) + +func TestGetEnvWithFallback(t *testing.T) { + tests := []struct { + name string + envVars map[string]string + keys []string + expected string + }{ + { + name: "first key exists", + envVars: map[string]string{"TEST_VAR_A": "value_a"}, + keys: []string{"TEST_VAR_A", "TEST_VAR_B"}, + expected: "value_a", + }, + { + name: "second key exists", + envVars: map[string]string{"TEST_VAR_B": "value_b"}, + keys: []string{"TEST_VAR_A", "TEST_VAR_B"}, + expected: "value_b", + }, + { + name: "first key takes precedence", + envVars: map[string]string{"TEST_VAR_A": "value_a", "TEST_VAR_B": "value_b"}, + keys: []string{"TEST_VAR_A", "TEST_VAR_B"}, + expected: "value_a", + }, + { + name: "no keys exist", + envVars: map[string]string{}, + keys: []string{"TEST_VAR_A", "TEST_VAR_B"}, + expected: "", + }, + { + name: "empty value is skipped", + envVars: map[string]string{"TEST_VAR_A": "", "TEST_VAR_B": "value_b"}, + keys: []string{"TEST_VAR_A", "TEST_VAR_B"}, + expected: "value_b", + }, + { + name: "no keys provided", + envVars: map[string]string{}, + keys: []string{}, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + clearEnvVars(t, tt.keys) + setEnvVars(t, tt.envVars) + + result := getEnvWithFallback(tt.keys...) + if result != tt.expected { + t.Errorf("got %q, want %q", result, tt.expected) + } + }) + } +} + +func TestPropagateAllProxy(t *testing.T) { + proxyVars := []string{"ALL_PROXY", "all_proxy", "HTTPS_PROXY", "https_proxy"} + + tests := []struct { + name string + envVars map[string]string + expectSet bool + expectedValue string + }{ + { + name: "ALL_PROXY propagates to HTTPS_PROXY", + envVars: map[string]string{"ALL_PROXY": "socks5h://proxy:1080"}, + expectSet: true, + expectedValue: "socks5h://proxy:1080", + }, + { + name: "all_proxy (lowercase) propagates to HTTPS_PROXY", + envVars: map[string]string{"all_proxy": "socks5h://proxy:1080"}, + expectSet: true, + expectedValue: "socks5h://proxy:1080", + }, + { + name: "ALL_PROXY takes precedence over all_proxy", + envVars: map[string]string{"ALL_PROXY": "http://upper", "all_proxy": "http://lower"}, + expectSet: true, + expectedValue: "http://upper", + }, + { + name: "HTTPS_PROXY already set - no propagation", + envVars: map[string]string{"ALL_PROXY": "http://all", "HTTPS_PROXY": "http://existing"}, + expectSet: true, + expectedValue: "http://existing", + }, + { + name: "https_proxy already set - no propagation", + envVars: map[string]string{"ALL_PROXY": "http://all", "https_proxy": "http://existing"}, + expectSet: true, + expectedValue: "http://existing", + }, + { + name: "no ALL_PROXY - no action", + envVars: map[string]string{}, + expectSet: false, + expectedValue: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + clearEnvVars(t, proxyVars) + setEnvVars(t, tt.envVars) + + propagateAllProxy() + + httpsProxy := getEnvWithFallback("HTTPS_PROXY", "https_proxy") + if tt.expectSet { + if httpsProxy != tt.expectedValue { + t.Errorf("HTTPS_PROXY = %q, want %q", httpsProxy, tt.expectedValue) + } + } else { + if httpsProxy != "" { + t.Errorf("HTTPS_PROXY should not be set, got %q", httpsProxy) + } + } + }) + } +} + +func clearEnvVars(t *testing.T, keys []string) { + t.Helper() + for _, key := range keys { + t.Setenv(key, "") + os.Unsetenv(key) + } +} + +func setEnvVars(t *testing.T, vars map[string]string) { + t.Helper() + for key, value := range vars { + t.Setenv(key, value) + } +} From c3b7b02519e020a9096494deb2056c9efb1dfbf4 Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Fri, 2 Jan 2026 06:10:32 +0000 Subject: [PATCH 2/9] feat: propagate ALL_PROXY to HTTP_PROXY and configure NO_PROXY Extend proxy support based on issue #69 feedback: - Propagate ALL_PROXY to both HTTP_PROXY and HTTPS_PROXY - Auto-configure NO_PROXY for AWS credential endpoints: - 169.254.169.254 (EC2 IMDS) - 169.254.170.2 (ECS Task Role) - 169.254.170.23 (EKS Pod Identity) - Preserve existing NO_PROXY entries, only add missing endpoints Refs #69, #67 --- cmd/claws/main.go | 108 +++++++++++++++++++++++++--- cmd/claws/proxy_test.go | 152 ++++++++++++++++++++++++++++++---------- 2 files changed, 214 insertions(+), 46 deletions(-) diff --git a/cmd/claws/main.go b/cmd/claws/main.go index fb251203..79b27c52 100644 --- a/cmd/claws/main.go +++ b/cmd/claws/main.go @@ -168,7 +168,8 @@ func printUsage() { fmt.Println() fmt.Println("Environment Variables:") fmt.Println(" CLAWS_READ_ONLY=1|true Enable read-only mode") - fmt.Println(" ALL_PROXY Propagated to HTTPS_PROXY if not set") + fmt.Println(" ALL_PROXY Propagated to HTTP_PROXY/HTTPS_PROXY if not set") + fmt.Println(" NO_PROXY auto-configured for AWS endpoints (IMDS, ECS, EKS)") } // getEnvWithFallback returns the first non-empty value from the given env var names. @@ -181,21 +182,110 @@ func getEnvWithFallback(names ...string) string { return "" } -// propagateAllProxy copies ALL_PROXY to HTTPS_PROXY if HTTPS_PROXY is not set. -// Go's net/http ignores ALL_PROXY and only reads HTTP_PROXY/HTTPS_PROXY. -// AWS APIs use HTTPS, so we only need HTTPS_PROXY. -// HTTP is only used for IMDS (169.254.169.254) which must NOT be proxied. +// awsNoProxyEndpoints lists AWS credential endpoints that must bypass proxy. +// - 169.254.169.254: EC2 IMDS +// - 169.254.170.2: ECS Task Role +// - 169.254.170.23: EKS Pod Identity +// TODO(#67): make configurable via config.yaml +var awsNoProxyEndpoints = []string{ + "169.254.169.254", + "169.254.170.2", + "169.254.170.23", +} + +// propagateAllProxy copies ALL_PROXY to HTTP_PROXY/HTTPS_PROXY if not set. +// Go's net/http ignores ALL_PROXY. When HTTP_PROXY is set, NO_PROXY is +// configured to exclude AWS credential endpoints (IMDS, ECS, EKS). func propagateAllProxy() { allProxy := getEnvWithFallback("ALL_PROXY", "all_proxy") if allProxy == "" { return } - if getEnvWithFallback("HTTPS_PROXY", "https_proxy") != "" { + + var propagated []string + + if getEnvWithFallback("HTTPS_PROXY", "https_proxy") == "" { + if err := os.Setenv("HTTPS_PROXY", allProxy); err != nil { + log.Warn("failed to set HTTPS_PROXY", "error", err) + } else { + propagated = append(propagated, "HTTPS_PROXY") + } + } + + if getEnvWithFallback("HTTP_PROXY", "http_proxy") == "" { + if err := os.Setenv("HTTP_PROXY", allProxy); err != nil { + log.Warn("failed to set HTTP_PROXY", "error", err) + } else { + propagated = append(propagated, "HTTP_PROXY") + configureNoProxy() + } + } + + if len(propagated) > 0 { + log.Info("propagated ALL_PROXY", "proxy", allProxy, "to", propagated) + } +} + +// configureNoProxy appends awsNoProxyEndpoints to NO_PROXY if missing. +func configureNoProxy() { + existing := getEnvWithFallback("NO_PROXY", "no_proxy") + + existingSet := make(map[string]bool) + if existing != "" { + for _, entry := range splitNoProxy(existing) { + existingSet[entry] = true + } + } + + var additions []string + for _, endpoint := range awsNoProxyEndpoints { + if !existingSet[endpoint] { + additions = append(additions, endpoint) + } + } + + if len(additions) == 0 { return } - if err := os.Setenv("HTTPS_PROXY", allProxy); err != nil { - log.Warn("failed to set HTTPS_PROXY", "error", err) + + newValue := existing + if newValue != "" { + newValue += "," + } + for i, endpoint := range additions { + if i > 0 { + newValue += "," + } + newValue += endpoint + } + + if err := os.Setenv("NO_PROXY", newValue); err != nil { + log.Warn("failed to set NO_PROXY", "error", err) return } - log.Info("propagated ALL_PROXY to HTTPS_PROXY", "proxy", allProxy) + log.Info("configured NO_PROXY for AWS endpoints", "added", additions) +} + +func splitNoProxy(value string) []string { + if value == "" { + return nil + } + var result []string + start := 0 + for i := 0; i <= len(value); i++ { + if i == len(value) || value[i] == ',' { + entry := value[start:i] + for len(entry) > 0 && entry[0] == ' ' { + entry = entry[1:] + } + for len(entry) > 0 && entry[len(entry)-1] == ' ' { + entry = entry[:len(entry)-1] + } + if entry != "" { + result = append(result, entry) + } + start = i + 1 + } + } + return result } diff --git a/cmd/claws/proxy_test.go b/cmd/claws/proxy_test.go index f84e61ba..09d29880 100644 --- a/cmd/claws/proxy_test.go +++ b/cmd/claws/proxy_test.go @@ -64,49 +64,100 @@ func TestGetEnvWithFallback(t *testing.T) { } func TestPropagateAllProxy(t *testing.T) { - proxyVars := []string{"ALL_PROXY", "all_proxy", "HTTPS_PROXY", "https_proxy"} + proxyVars := []string{ + "ALL_PROXY", "all_proxy", + "HTTP_PROXY", "http_proxy", + "HTTPS_PROXY", "https_proxy", + "NO_PROXY", "no_proxy", + } tests := []struct { - name string - envVars map[string]string - expectSet bool - expectedValue string + name string + envVars map[string]string + wantHTTPS string + wantHTTP string + wantNoProxy string + noProxyChanged bool }{ { - name: "ALL_PROXY propagates to HTTPS_PROXY", - envVars: map[string]string{"ALL_PROXY": "socks5h://proxy:1080"}, - expectSet: true, - expectedValue: "socks5h://proxy:1080", + name: "ALL_PROXY propagates to both HTTP and HTTPS", + envVars: map[string]string{"ALL_PROXY": "socks5h://proxy:1080"}, + wantHTTPS: "socks5h://proxy:1080", + wantHTTP: "socks5h://proxy:1080", + wantNoProxy: "169.254.169.254,169.254.170.2,169.254.170.23", + noProxyChanged: true, + }, + { + name: "all_proxy (lowercase) propagates", + envVars: map[string]string{"all_proxy": "socks5h://proxy:1080"}, + wantHTTPS: "socks5h://proxy:1080", + wantHTTP: "socks5h://proxy:1080", + wantNoProxy: "169.254.169.254,169.254.170.2,169.254.170.23", + noProxyChanged: true, }, { - name: "all_proxy (lowercase) propagates to HTTPS_PROXY", - envVars: map[string]string{"all_proxy": "socks5h://proxy:1080"}, - expectSet: true, - expectedValue: "socks5h://proxy:1080", + name: "ALL_PROXY takes precedence over all_proxy", + envVars: map[string]string{"ALL_PROXY": "http://upper", "all_proxy": "http://lower"}, + wantHTTPS: "http://upper", + wantHTTP: "http://upper", + wantNoProxy: "169.254.169.254,169.254.170.2,169.254.170.23", + noProxyChanged: true, }, { - name: "ALL_PROXY takes precedence over all_proxy", - envVars: map[string]string{"ALL_PROXY": "http://upper", "all_proxy": "http://lower"}, - expectSet: true, - expectedValue: "http://upper", + name: "HTTPS_PROXY already set - only HTTP propagated", + envVars: map[string]string{"ALL_PROXY": "http://all", "HTTPS_PROXY": "http://existing"}, + wantHTTPS: "http://existing", + wantHTTP: "http://all", + wantNoProxy: "169.254.169.254,169.254.170.2,169.254.170.23", + noProxyChanged: true, }, { - name: "HTTPS_PROXY already set - no propagation", - envVars: map[string]string{"ALL_PROXY": "http://all", "HTTPS_PROXY": "http://existing"}, - expectSet: true, - expectedValue: "http://existing", + name: "HTTP_PROXY already set - only HTTPS propagated, NO_PROXY unchanged", + envVars: map[string]string{"ALL_PROXY": "http://all", "HTTP_PROXY": "http://existing"}, + wantHTTPS: "http://all", + wantHTTP: "http://existing", + wantNoProxy: "", + noProxyChanged: false, }, { - name: "https_proxy already set - no propagation", - envVars: map[string]string{"ALL_PROXY": "http://all", "https_proxy": "http://existing"}, - expectSet: true, - expectedValue: "http://existing", + name: "both already set - no propagation", + envVars: map[string]string{"ALL_PROXY": "http://all", "HTTP_PROXY": "http://h", "HTTPS_PROXY": "http://s"}, + wantHTTPS: "http://s", + wantHTTP: "http://h", + wantNoProxy: "", + noProxyChanged: false, }, { - name: "no ALL_PROXY - no action", - envVars: map[string]string{}, - expectSet: false, - expectedValue: "", + name: "no ALL_PROXY - no action", + envVars: map[string]string{}, + wantHTTPS: "", + wantHTTP: "", + wantNoProxy: "", + noProxyChanged: false, + }, + { + name: "existing NO_PROXY is preserved and extended", + envVars: map[string]string{"ALL_PROXY": "http://proxy", "NO_PROXY": "localhost,127.0.0.1"}, + wantHTTPS: "http://proxy", + wantHTTP: "http://proxy", + wantNoProxy: "localhost,127.0.0.1,169.254.169.254,169.254.170.2,169.254.170.23", + noProxyChanged: true, + }, + { + name: "NO_PROXY with partial AWS endpoints - only missing added", + envVars: map[string]string{"ALL_PROXY": "http://proxy", "NO_PROXY": "169.254.169.254"}, + wantHTTPS: "http://proxy", + wantHTTP: "http://proxy", + wantNoProxy: "169.254.169.254,169.254.170.2,169.254.170.23", + noProxyChanged: true, + }, + { + name: "NO_PROXY already has all AWS endpoints - no change", + envVars: map[string]string{"ALL_PROXY": "http://proxy", "NO_PROXY": "169.254.169.254,169.254.170.2,169.254.170.23"}, + wantHTTPS: "http://proxy", + wantHTTP: "http://proxy", + wantNoProxy: "169.254.169.254,169.254.170.2,169.254.170.23", + noProxyChanged: false, }, } @@ -117,14 +168,41 @@ func TestPropagateAllProxy(t *testing.T) { propagateAllProxy() - httpsProxy := getEnvWithFallback("HTTPS_PROXY", "https_proxy") - if tt.expectSet { - if httpsProxy != tt.expectedValue { - t.Errorf("HTTPS_PROXY = %q, want %q", httpsProxy, tt.expectedValue) - } - } else { - if httpsProxy != "" { - t.Errorf("HTTPS_PROXY should not be set, got %q", httpsProxy) + if got := getEnvWithFallback("HTTPS_PROXY", "https_proxy"); got != tt.wantHTTPS { + t.Errorf("HTTPS_PROXY = %q, want %q", got, tt.wantHTTPS) + } + if got := getEnvWithFallback("HTTP_PROXY", "http_proxy"); got != tt.wantHTTP { + t.Errorf("HTTP_PROXY = %q, want %q", got, tt.wantHTTP) + } + if got := getEnvWithFallback("NO_PROXY", "no_proxy"); got != tt.wantNoProxy { + t.Errorf("NO_PROXY = %q, want %q", got, tt.wantNoProxy) + } + }) + } +} + +func TestSplitNoProxy(t *testing.T) { + tests := []struct { + input string + want []string + }{ + {"", nil}, + {"localhost", []string{"localhost"}}, + {"a,b,c", []string{"a", "b", "c"}}, + {"a, b, c", []string{"a", "b", "c"}}, + {" a , b , c ", []string{"a", "b", "c"}}, + {"a,,b", []string{"a", "b"}}, + } + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got := splitNoProxy(tt.input) + if len(got) != len(tt.want) { + t.Errorf("splitNoProxy(%q) = %v, want %v", tt.input, got, tt.want) + return + } + for i := range got { + if got[i] != tt.want[i] { + t.Errorf("splitNoProxy(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i]) } } }) From c1f3b3a9210356e600ef8834310cffbf39d6e648 Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Fri, 2 Jan 2026 07:10:28 +0000 Subject: [PATCH 3/9] fix: default NO_PROXY to IMDS only and remove proxy value from logs - NO_PROXY now only includes EC2 IMDS (169.254.169.254) by default - ECS/EKS endpoints can be added via config (TODO #67) - Remove proxy URL from log output to avoid credential exposure --- cmd/claws/main.go | 12 ++++-------- cmd/claws/proxy_test.go | 22 +++++++--------------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/cmd/claws/main.go b/cmd/claws/main.go index 79b27c52..cbccc806 100644 --- a/cmd/claws/main.go +++ b/cmd/claws/main.go @@ -169,7 +169,7 @@ func printUsage() { fmt.Println("Environment Variables:") fmt.Println(" CLAWS_READ_ONLY=1|true Enable read-only mode") fmt.Println(" ALL_PROXY Propagated to HTTP_PROXY/HTTPS_PROXY if not set") - fmt.Println(" NO_PROXY auto-configured for AWS endpoints (IMDS, ECS, EKS)") + fmt.Println(" NO_PROXY auto-configured for EC2 IMDS (169.254.169.254)") } // getEnvWithFallback returns the first non-empty value from the given env var names. @@ -183,14 +183,10 @@ func getEnvWithFallback(names ...string) string { } // awsNoProxyEndpoints lists AWS credential endpoints that must bypass proxy. -// - 169.254.169.254: EC2 IMDS -// - 169.254.170.2: ECS Task Role -// - 169.254.170.23: EKS Pod Identity +// Default: EC2 IMDS only. ECS/EKS endpoints can be added via config. // TODO(#67): make configurable via config.yaml var awsNoProxyEndpoints = []string{ - "169.254.169.254", - "169.254.170.2", - "169.254.170.23", + "169.254.169.254", // EC2 IMDS } // propagateAllProxy copies ALL_PROXY to HTTP_PROXY/HTTPS_PROXY if not set. @@ -222,7 +218,7 @@ func propagateAllProxy() { } if len(propagated) > 0 { - log.Info("propagated ALL_PROXY", "proxy", allProxy, "to", propagated) + log.Info("propagated ALL_PROXY", "to", propagated) } } diff --git a/cmd/claws/proxy_test.go b/cmd/claws/proxy_test.go index 09d29880..ff58546d 100644 --- a/cmd/claws/proxy_test.go +++ b/cmd/claws/proxy_test.go @@ -84,7 +84,7 @@ func TestPropagateAllProxy(t *testing.T) { envVars: map[string]string{"ALL_PROXY": "socks5h://proxy:1080"}, wantHTTPS: "socks5h://proxy:1080", wantHTTP: "socks5h://proxy:1080", - wantNoProxy: "169.254.169.254,169.254.170.2,169.254.170.23", + wantNoProxy: "169.254.169.254", noProxyChanged: true, }, { @@ -92,7 +92,7 @@ func TestPropagateAllProxy(t *testing.T) { envVars: map[string]string{"all_proxy": "socks5h://proxy:1080"}, wantHTTPS: "socks5h://proxy:1080", wantHTTP: "socks5h://proxy:1080", - wantNoProxy: "169.254.169.254,169.254.170.2,169.254.170.23", + wantNoProxy: "169.254.169.254", noProxyChanged: true, }, { @@ -100,7 +100,7 @@ func TestPropagateAllProxy(t *testing.T) { envVars: map[string]string{"ALL_PROXY": "http://upper", "all_proxy": "http://lower"}, wantHTTPS: "http://upper", wantHTTP: "http://upper", - wantNoProxy: "169.254.169.254,169.254.170.2,169.254.170.23", + wantNoProxy: "169.254.169.254", noProxyChanged: true, }, { @@ -108,7 +108,7 @@ func TestPropagateAllProxy(t *testing.T) { envVars: map[string]string{"ALL_PROXY": "http://all", "HTTPS_PROXY": "http://existing"}, wantHTTPS: "http://existing", wantHTTP: "http://all", - wantNoProxy: "169.254.169.254,169.254.170.2,169.254.170.23", + wantNoProxy: "169.254.169.254", noProxyChanged: true, }, { @@ -140,23 +140,15 @@ func TestPropagateAllProxy(t *testing.T) { envVars: map[string]string{"ALL_PROXY": "http://proxy", "NO_PROXY": "localhost,127.0.0.1"}, wantHTTPS: "http://proxy", wantHTTP: "http://proxy", - wantNoProxy: "localhost,127.0.0.1,169.254.169.254,169.254.170.2,169.254.170.23", + wantNoProxy: "localhost,127.0.0.1,169.254.169.254", noProxyChanged: true, }, { - name: "NO_PROXY with partial AWS endpoints - only missing added", + name: "NO_PROXY already has IMDS - no change", envVars: map[string]string{"ALL_PROXY": "http://proxy", "NO_PROXY": "169.254.169.254"}, wantHTTPS: "http://proxy", wantHTTP: "http://proxy", - wantNoProxy: "169.254.169.254,169.254.170.2,169.254.170.23", - noProxyChanged: true, - }, - { - name: "NO_PROXY already has all AWS endpoints - no change", - envVars: map[string]string{"ALL_PROXY": "http://proxy", "NO_PROXY": "169.254.169.254,169.254.170.2,169.254.170.23"}, - wantHTTPS: "http://proxy", - wantHTTP: "http://proxy", - wantNoProxy: "169.254.169.254,169.254.170.2,169.254.170.23", + wantNoProxy: "169.254.169.254", noProxyChanged: false, }, } From 6ae4ae6b389dc8b8a54e6c1315fb26d7187467fc Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Fri, 2 Jan 2026 07:21:41 +0000 Subject: [PATCH 4/9] refactor: simplify splitNoProxy and use tagged switch in parseFlags --- cmd/claws/main.go | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/cmd/claws/main.go b/cmd/claws/main.go index cbccc806..dfd8844e 100644 --- a/cmd/claws/main.go +++ b/cmd/claws/main.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "os" + "strings" tea "charm.land/bubbletea/v2" @@ -103,30 +104,29 @@ func parseFlags() cliOptions { args := os.Args[1:] for i := 0; i < len(args); i++ { - arg := args[i] - switch { - case arg == "-p" || arg == "--profile": + switch args[i] { + case "-p", "--profile": if i+1 < len(args) { i++ opts.profile = args[i] } - case arg == "-r" || arg == "--region": + case "-r", "--region": if i+1 < len(args) { i++ opts.region = args[i] } - case arg == "-ro" || arg == "--read-only": + case "-ro", "--read-only": opts.readOnly = true - case arg == "-e" || arg == "--env": + case "-e", "--env": opts.envCreds = true - case arg == "-l" || arg == "--log-file": + case "-l", "--log-file": if i+1 < len(args) { i++ opts.logFile = args[i] } - case arg == "-h" || arg == "--help": + case "-h", "--help": showHelp = true - case arg == "-v" || arg == "--version": + case "-v", "--version": showVersion = true } } @@ -266,21 +266,11 @@ func splitNoProxy(value string) []string { if value == "" { return nil } - var result []string - start := 0 - for i := 0; i <= len(value); i++ { - if i == len(value) || value[i] == ',' { - entry := value[start:i] - for len(entry) > 0 && entry[0] == ' ' { - entry = entry[1:] - } - for len(entry) > 0 && entry[len(entry)-1] == ' ' { - entry = entry[:len(entry)-1] - } - if entry != "" { - result = append(result, entry) - } - start = i + 1 + parts := strings.Split(value, ",") + result := make([]string, 0, len(parts)) + for _, p := range parts { + if s := strings.TrimSpace(p); s != "" { + result = append(result, s) } } return result From 4d1ba521700a453b13e081dd4c10c92c79af71aa Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Fri, 2 Jan 2026 07:44:18 +0000 Subject: [PATCH 5/9] refactor: separate NO_PROXY config from ALL_PROXY propagation configureNoProxy() now runs independently when any proxy is set, not tied to ALL_PROXY propagation. Cleaner separation of concerns. --- cmd/claws/main.go | 7 +- cmd/claws/proxy_test.go | 156 +++++++++++++++++++++++----------------- 2 files changed, 98 insertions(+), 65 deletions(-) diff --git a/cmd/claws/main.go b/cmd/claws/main.go index dfd8844e..999aa02f 100644 --- a/cmd/claws/main.go +++ b/cmd/claws/main.go @@ -21,6 +21,7 @@ var version = "dev" func main() { propagateAllProxy() + configureNoProxy() opts := parseFlags() @@ -213,7 +214,6 @@ func propagateAllProxy() { log.Warn("failed to set HTTP_PROXY", "error", err) } else { propagated = append(propagated, "HTTP_PROXY") - configureNoProxy() } } @@ -224,6 +224,11 @@ func propagateAllProxy() { // configureNoProxy appends awsNoProxyEndpoints to NO_PROXY if missing. func configureNoProxy() { + if getEnvWithFallback("HTTP_PROXY", "http_proxy") == "" && + getEnvWithFallback("HTTPS_PROXY", "https_proxy") == "" { + return + } + existing := getEnvWithFallback("NO_PROXY", "no_proxy") existingSet := make(map[string]bool) diff --git a/cmd/claws/proxy_test.go b/cmd/claws/proxy_test.go index ff58546d..8569dd2a 100644 --- a/cmd/claws/proxy_test.go +++ b/cmd/claws/proxy_test.go @@ -72,84 +72,52 @@ func TestPropagateAllProxy(t *testing.T) { } tests := []struct { - name string - envVars map[string]string - wantHTTPS string - wantHTTP string - wantNoProxy string - noProxyChanged bool + name string + envVars map[string]string + wantHTTPS string + wantHTTP string }{ { - name: "ALL_PROXY propagates to both HTTP and HTTPS", - envVars: map[string]string{"ALL_PROXY": "socks5h://proxy:1080"}, - wantHTTPS: "socks5h://proxy:1080", - wantHTTP: "socks5h://proxy:1080", - wantNoProxy: "169.254.169.254", - noProxyChanged: true, + name: "ALL_PROXY propagates to both HTTP and HTTPS", + envVars: map[string]string{"ALL_PROXY": "socks5h://proxy:1080"}, + wantHTTPS: "socks5h://proxy:1080", + wantHTTP: "socks5h://proxy:1080", }, { - name: "all_proxy (lowercase) propagates", - envVars: map[string]string{"all_proxy": "socks5h://proxy:1080"}, - wantHTTPS: "socks5h://proxy:1080", - wantHTTP: "socks5h://proxy:1080", - wantNoProxy: "169.254.169.254", - noProxyChanged: true, + name: "all_proxy (lowercase) propagates", + envVars: map[string]string{"all_proxy": "socks5h://proxy:1080"}, + wantHTTPS: "socks5h://proxy:1080", + wantHTTP: "socks5h://proxy:1080", }, { - name: "ALL_PROXY takes precedence over all_proxy", - envVars: map[string]string{"ALL_PROXY": "http://upper", "all_proxy": "http://lower"}, - wantHTTPS: "http://upper", - wantHTTP: "http://upper", - wantNoProxy: "169.254.169.254", - noProxyChanged: true, + name: "ALL_PROXY takes precedence over all_proxy", + envVars: map[string]string{"ALL_PROXY": "http://upper", "all_proxy": "http://lower"}, + wantHTTPS: "http://upper", + wantHTTP: "http://upper", }, { - name: "HTTPS_PROXY already set - only HTTP propagated", - envVars: map[string]string{"ALL_PROXY": "http://all", "HTTPS_PROXY": "http://existing"}, - wantHTTPS: "http://existing", - wantHTTP: "http://all", - wantNoProxy: "169.254.169.254", - noProxyChanged: true, + name: "HTTPS_PROXY already set - only HTTP propagated", + envVars: map[string]string{"ALL_PROXY": "http://all", "HTTPS_PROXY": "http://existing"}, + wantHTTPS: "http://existing", + wantHTTP: "http://all", }, { - name: "HTTP_PROXY already set - only HTTPS propagated, NO_PROXY unchanged", - envVars: map[string]string{"ALL_PROXY": "http://all", "HTTP_PROXY": "http://existing"}, - wantHTTPS: "http://all", - wantHTTP: "http://existing", - wantNoProxy: "", - noProxyChanged: false, + name: "HTTP_PROXY already set - only HTTPS propagated", + envVars: map[string]string{"ALL_PROXY": "http://all", "HTTP_PROXY": "http://existing"}, + wantHTTPS: "http://all", + wantHTTP: "http://existing", }, { - name: "both already set - no propagation", - envVars: map[string]string{"ALL_PROXY": "http://all", "HTTP_PROXY": "http://h", "HTTPS_PROXY": "http://s"}, - wantHTTPS: "http://s", - wantHTTP: "http://h", - wantNoProxy: "", - noProxyChanged: false, + name: "both already set - no propagation", + envVars: map[string]string{"ALL_PROXY": "http://all", "HTTP_PROXY": "http://h", "HTTPS_PROXY": "http://s"}, + wantHTTPS: "http://s", + wantHTTP: "http://h", }, { - name: "no ALL_PROXY - no action", - envVars: map[string]string{}, - wantHTTPS: "", - wantHTTP: "", - wantNoProxy: "", - noProxyChanged: false, - }, - { - name: "existing NO_PROXY is preserved and extended", - envVars: map[string]string{"ALL_PROXY": "http://proxy", "NO_PROXY": "localhost,127.0.0.1"}, - wantHTTPS: "http://proxy", - wantHTTP: "http://proxy", - wantNoProxy: "localhost,127.0.0.1,169.254.169.254", - noProxyChanged: true, - }, - { - name: "NO_PROXY already has IMDS - no change", - envVars: map[string]string{"ALL_PROXY": "http://proxy", "NO_PROXY": "169.254.169.254"}, - wantHTTPS: "http://proxy", - wantHTTP: "http://proxy", - wantNoProxy: "169.254.169.254", - noProxyChanged: false, + name: "no ALL_PROXY - no action", + envVars: map[string]string{}, + wantHTTPS: "", + wantHTTP: "", }, } @@ -166,6 +134,66 @@ func TestPropagateAllProxy(t *testing.T) { if got := getEnvWithFallback("HTTP_PROXY", "http_proxy"); got != tt.wantHTTP { t.Errorf("HTTP_PROXY = %q, want %q", got, tt.wantHTTP) } + }) + } +} + +func TestConfigureNoProxy(t *testing.T) { + proxyVars := []string{ + "HTTP_PROXY", "http_proxy", + "HTTPS_PROXY", "https_proxy", + "NO_PROXY", "no_proxy", + } + + tests := []struct { + name string + envVars map[string]string + wantNoProxy string + }{ + { + name: "no proxy set - no action", + envVars: map[string]string{}, + wantNoProxy: "", + }, + { + name: "HTTP_PROXY set - adds IMDS", + envVars: map[string]string{"HTTP_PROXY": "http://proxy:8080"}, + wantNoProxy: "169.254.169.254", + }, + { + name: "HTTPS_PROXY set - adds IMDS", + envVars: map[string]string{"HTTPS_PROXY": "http://proxy:8080"}, + wantNoProxy: "169.254.169.254", + }, + { + name: "both proxies set - adds IMDS", + envVars: map[string]string{"HTTP_PROXY": "http://h", "HTTPS_PROXY": "http://s"}, + wantNoProxy: "169.254.169.254", + }, + { + name: "existing NO_PROXY preserved and extended", + envVars: map[string]string{"HTTP_PROXY": "http://proxy", "NO_PROXY": "localhost,127.0.0.1"}, + wantNoProxy: "localhost,127.0.0.1,169.254.169.254", + }, + { + name: "NO_PROXY already has IMDS - no change", + envVars: map[string]string{"HTTP_PROXY": "http://proxy", "NO_PROXY": "169.254.169.254"}, + wantNoProxy: "169.254.169.254", + }, + { + name: "lowercase http_proxy triggers config", + envVars: map[string]string{"http_proxy": "http://proxy"}, + wantNoProxy: "169.254.169.254", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + clearEnvVars(t, proxyVars) + setEnvVars(t, tt.envVars) + + configureNoProxy() + if got := getEnvWithFallback("NO_PROXY", "no_proxy"); got != tt.wantNoProxy { t.Errorf("NO_PROXY = %q, want %q", got, tt.wantNoProxy) } From fc4941369ae8f3388d83f84f6ad24d1ad538447f Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Fri, 2 Jan 2026 07:56:05 +0000 Subject: [PATCH 6/9] refactor: remove lowercase env var support for simplicity --- cmd/claws/main.go | 25 +++-------- cmd/claws/proxy_test.go | 95 +++++------------------------------------ 2 files changed, 17 insertions(+), 103 deletions(-) diff --git a/cmd/claws/main.go b/cmd/claws/main.go index 999aa02f..6af92381 100644 --- a/cmd/claws/main.go +++ b/cmd/claws/main.go @@ -173,16 +173,6 @@ func printUsage() { fmt.Println(" NO_PROXY auto-configured for EC2 IMDS (169.254.169.254)") } -// getEnvWithFallback returns the first non-empty value from the given env var names. -func getEnvWithFallback(names ...string) string { - for _, name := range names { - if v := os.Getenv(name); v != "" { - return v - } - } - return "" -} - // awsNoProxyEndpoints lists AWS credential endpoints that must bypass proxy. // Default: EC2 IMDS only. ECS/EKS endpoints can be added via config. // TODO(#67): make configurable via config.yaml @@ -194,14 +184,14 @@ var awsNoProxyEndpoints = []string{ // Go's net/http ignores ALL_PROXY. When HTTP_PROXY is set, NO_PROXY is // configured to exclude AWS credential endpoints (IMDS, ECS, EKS). func propagateAllProxy() { - allProxy := getEnvWithFallback("ALL_PROXY", "all_proxy") + allProxy := os.Getenv("ALL_PROXY") if allProxy == "" { return } var propagated []string - if getEnvWithFallback("HTTPS_PROXY", "https_proxy") == "" { + if os.Getenv("HTTPS_PROXY") == "" { if err := os.Setenv("HTTPS_PROXY", allProxy); err != nil { log.Warn("failed to set HTTPS_PROXY", "error", err) } else { @@ -209,7 +199,7 @@ func propagateAllProxy() { } } - if getEnvWithFallback("HTTP_PROXY", "http_proxy") == "" { + if os.Getenv("HTTP_PROXY") == "" { if err := os.Setenv("HTTP_PROXY", allProxy); err != nil { log.Warn("failed to set HTTP_PROXY", "error", err) } else { @@ -218,18 +208,17 @@ func propagateAllProxy() { } if len(propagated) > 0 { - log.Info("propagated ALL_PROXY", "to", propagated) + log.Debug("propagated ALL_PROXY", "to", propagated) } } // configureNoProxy appends awsNoProxyEndpoints to NO_PROXY if missing. func configureNoProxy() { - if getEnvWithFallback("HTTP_PROXY", "http_proxy") == "" && - getEnvWithFallback("HTTPS_PROXY", "https_proxy") == "" { + if os.Getenv("HTTP_PROXY") == "" && os.Getenv("HTTPS_PROXY") == "" { return } - existing := getEnvWithFallback("NO_PROXY", "no_proxy") + existing := os.Getenv("NO_PROXY") existingSet := make(map[string]bool) if existing != "" { @@ -264,7 +253,7 @@ func configureNoProxy() { log.Warn("failed to set NO_PROXY", "error", err) return } - log.Info("configured NO_PROXY for AWS endpoints", "added", additions) + log.Debug("configured NO_PROXY for AWS endpoints", "added", additions) } func splitNoProxy(value string) []string { diff --git a/cmd/claws/proxy_test.go b/cmd/claws/proxy_test.go index 8569dd2a..cf294f2d 100644 --- a/cmd/claws/proxy_test.go +++ b/cmd/claws/proxy_test.go @@ -5,70 +5,12 @@ import ( "testing" ) -func TestGetEnvWithFallback(t *testing.T) { - tests := []struct { - name string - envVars map[string]string - keys []string - expected string - }{ - { - name: "first key exists", - envVars: map[string]string{"TEST_VAR_A": "value_a"}, - keys: []string{"TEST_VAR_A", "TEST_VAR_B"}, - expected: "value_a", - }, - { - name: "second key exists", - envVars: map[string]string{"TEST_VAR_B": "value_b"}, - keys: []string{"TEST_VAR_A", "TEST_VAR_B"}, - expected: "value_b", - }, - { - name: "first key takes precedence", - envVars: map[string]string{"TEST_VAR_A": "value_a", "TEST_VAR_B": "value_b"}, - keys: []string{"TEST_VAR_A", "TEST_VAR_B"}, - expected: "value_a", - }, - { - name: "no keys exist", - envVars: map[string]string{}, - keys: []string{"TEST_VAR_A", "TEST_VAR_B"}, - expected: "", - }, - { - name: "empty value is skipped", - envVars: map[string]string{"TEST_VAR_A": "", "TEST_VAR_B": "value_b"}, - keys: []string{"TEST_VAR_A", "TEST_VAR_B"}, - expected: "value_b", - }, - { - name: "no keys provided", - envVars: map[string]string{}, - keys: []string{}, - expected: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - clearEnvVars(t, tt.keys) - setEnvVars(t, tt.envVars) - - result := getEnvWithFallback(tt.keys...) - if result != tt.expected { - t.Errorf("got %q, want %q", result, tt.expected) - } - }) - } -} - func TestPropagateAllProxy(t *testing.T) { proxyVars := []string{ - "ALL_PROXY", "all_proxy", - "HTTP_PROXY", "http_proxy", - "HTTPS_PROXY", "https_proxy", - "NO_PROXY", "no_proxy", + "ALL_PROXY", + "HTTP_PROXY", + "HTTPS_PROXY", + "NO_PROXY", } tests := []struct { @@ -83,18 +25,6 @@ func TestPropagateAllProxy(t *testing.T) { wantHTTPS: "socks5h://proxy:1080", wantHTTP: "socks5h://proxy:1080", }, - { - name: "all_proxy (lowercase) propagates", - envVars: map[string]string{"all_proxy": "socks5h://proxy:1080"}, - wantHTTPS: "socks5h://proxy:1080", - wantHTTP: "socks5h://proxy:1080", - }, - { - name: "ALL_PROXY takes precedence over all_proxy", - envVars: map[string]string{"ALL_PROXY": "http://upper", "all_proxy": "http://lower"}, - wantHTTPS: "http://upper", - wantHTTP: "http://upper", - }, { name: "HTTPS_PROXY already set - only HTTP propagated", envVars: map[string]string{"ALL_PROXY": "http://all", "HTTPS_PROXY": "http://existing"}, @@ -128,10 +58,10 @@ func TestPropagateAllProxy(t *testing.T) { propagateAllProxy() - if got := getEnvWithFallback("HTTPS_PROXY", "https_proxy"); got != tt.wantHTTPS { + if got := os.Getenv("HTTPS_PROXY"); got != tt.wantHTTPS { t.Errorf("HTTPS_PROXY = %q, want %q", got, tt.wantHTTPS) } - if got := getEnvWithFallback("HTTP_PROXY", "http_proxy"); got != tt.wantHTTP { + if got := os.Getenv("HTTP_PROXY"); got != tt.wantHTTP { t.Errorf("HTTP_PROXY = %q, want %q", got, tt.wantHTTP) } }) @@ -140,9 +70,9 @@ func TestPropagateAllProxy(t *testing.T) { func TestConfigureNoProxy(t *testing.T) { proxyVars := []string{ - "HTTP_PROXY", "http_proxy", - "HTTPS_PROXY", "https_proxy", - "NO_PROXY", "no_proxy", + "HTTP_PROXY", + "HTTPS_PROXY", + "NO_PROXY", } tests := []struct { @@ -180,11 +110,6 @@ func TestConfigureNoProxy(t *testing.T) { envVars: map[string]string{"HTTP_PROXY": "http://proxy", "NO_PROXY": "169.254.169.254"}, wantNoProxy: "169.254.169.254", }, - { - name: "lowercase http_proxy triggers config", - envVars: map[string]string{"http_proxy": "http://proxy"}, - wantNoProxy: "169.254.169.254", - }, } for _, tt := range tests { @@ -194,7 +119,7 @@ func TestConfigureNoProxy(t *testing.T) { configureNoProxy() - if got := getEnvWithFallback("NO_PROXY", "no_proxy"); got != tt.wantNoProxy { + if got := os.Getenv("NO_PROXY"); got != tt.wantNoProxy { t.Errorf("NO_PROXY = %q, want %q", got, tt.wantNoProxy) } }) From b24e1520ec538a742f33bd93169c9b6ade60b1d3 Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Fri, 2 Jan 2026 09:33:03 +0000 Subject: [PATCH 7/9] refactor: remove NO_PROXY auto-configuration --- cmd/claws/main.go | 71 +-------------------------------- cmd/claws/proxy_test.go | 87 ----------------------------------------- 2 files changed, 1 insertion(+), 157 deletions(-) diff --git a/cmd/claws/main.go b/cmd/claws/main.go index 6af92381..7b73215a 100644 --- a/cmd/claws/main.go +++ b/cmd/claws/main.go @@ -6,7 +6,6 @@ import ( "context" "fmt" "os" - "strings" tea "charm.land/bubbletea/v2" @@ -21,7 +20,6 @@ var version = "dev" func main() { propagateAllProxy() - configureNoProxy() opts := parseFlags() @@ -170,19 +168,10 @@ func printUsage() { fmt.Println("Environment Variables:") fmt.Println(" CLAWS_READ_ONLY=1|true Enable read-only mode") fmt.Println(" ALL_PROXY Propagated to HTTP_PROXY/HTTPS_PROXY if not set") - fmt.Println(" NO_PROXY auto-configured for EC2 IMDS (169.254.169.254)") -} - -// awsNoProxyEndpoints lists AWS credential endpoints that must bypass proxy. -// Default: EC2 IMDS only. ECS/EKS endpoints can be added via config. -// TODO(#67): make configurable via config.yaml -var awsNoProxyEndpoints = []string{ - "169.254.169.254", // EC2 IMDS } // propagateAllProxy copies ALL_PROXY to HTTP_PROXY/HTTPS_PROXY if not set. -// Go's net/http ignores ALL_PROXY. When HTTP_PROXY is set, NO_PROXY is -// configured to exclude AWS credential endpoints (IMDS, ECS, EKS). +// Go's net/http ignores ALL_PROXY, so we propagate it to the standard vars. func propagateAllProxy() { allProxy := os.Getenv("ALL_PROXY") if allProxy == "" { @@ -211,61 +200,3 @@ func propagateAllProxy() { log.Debug("propagated ALL_PROXY", "to", propagated) } } - -// configureNoProxy appends awsNoProxyEndpoints to NO_PROXY if missing. -func configureNoProxy() { - if os.Getenv("HTTP_PROXY") == "" && os.Getenv("HTTPS_PROXY") == "" { - return - } - - existing := os.Getenv("NO_PROXY") - - existingSet := make(map[string]bool) - if existing != "" { - for _, entry := range splitNoProxy(existing) { - existingSet[entry] = true - } - } - - var additions []string - for _, endpoint := range awsNoProxyEndpoints { - if !existingSet[endpoint] { - additions = append(additions, endpoint) - } - } - - if len(additions) == 0 { - return - } - - newValue := existing - if newValue != "" { - newValue += "," - } - for i, endpoint := range additions { - if i > 0 { - newValue += "," - } - newValue += endpoint - } - - if err := os.Setenv("NO_PROXY", newValue); err != nil { - log.Warn("failed to set NO_PROXY", "error", err) - return - } - log.Debug("configured NO_PROXY for AWS endpoints", "added", additions) -} - -func splitNoProxy(value string) []string { - if value == "" { - return nil - } - parts := strings.Split(value, ",") - result := make([]string, 0, len(parts)) - for _, p := range parts { - if s := strings.TrimSpace(p); s != "" { - result = append(result, s) - } - } - return result -} diff --git a/cmd/claws/proxy_test.go b/cmd/claws/proxy_test.go index cf294f2d..ebe7c21a 100644 --- a/cmd/claws/proxy_test.go +++ b/cmd/claws/proxy_test.go @@ -10,7 +10,6 @@ func TestPropagateAllProxy(t *testing.T) { "ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY", - "NO_PROXY", } tests := []struct { @@ -68,92 +67,6 @@ func TestPropagateAllProxy(t *testing.T) { } } -func TestConfigureNoProxy(t *testing.T) { - proxyVars := []string{ - "HTTP_PROXY", - "HTTPS_PROXY", - "NO_PROXY", - } - - tests := []struct { - name string - envVars map[string]string - wantNoProxy string - }{ - { - name: "no proxy set - no action", - envVars: map[string]string{}, - wantNoProxy: "", - }, - { - name: "HTTP_PROXY set - adds IMDS", - envVars: map[string]string{"HTTP_PROXY": "http://proxy:8080"}, - wantNoProxy: "169.254.169.254", - }, - { - name: "HTTPS_PROXY set - adds IMDS", - envVars: map[string]string{"HTTPS_PROXY": "http://proxy:8080"}, - wantNoProxy: "169.254.169.254", - }, - { - name: "both proxies set - adds IMDS", - envVars: map[string]string{"HTTP_PROXY": "http://h", "HTTPS_PROXY": "http://s"}, - wantNoProxy: "169.254.169.254", - }, - { - name: "existing NO_PROXY preserved and extended", - envVars: map[string]string{"HTTP_PROXY": "http://proxy", "NO_PROXY": "localhost,127.0.0.1"}, - wantNoProxy: "localhost,127.0.0.1,169.254.169.254", - }, - { - name: "NO_PROXY already has IMDS - no change", - envVars: map[string]string{"HTTP_PROXY": "http://proxy", "NO_PROXY": "169.254.169.254"}, - wantNoProxy: "169.254.169.254", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - clearEnvVars(t, proxyVars) - setEnvVars(t, tt.envVars) - - configureNoProxy() - - if got := os.Getenv("NO_PROXY"); got != tt.wantNoProxy { - t.Errorf("NO_PROXY = %q, want %q", got, tt.wantNoProxy) - } - }) - } -} - -func TestSplitNoProxy(t *testing.T) { - tests := []struct { - input string - want []string - }{ - {"", nil}, - {"localhost", []string{"localhost"}}, - {"a,b,c", []string{"a", "b", "c"}}, - {"a, b, c", []string{"a", "b", "c"}}, - {" a , b , c ", []string{"a", "b", "c"}}, - {"a,,b", []string{"a", "b"}}, - } - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - got := splitNoProxy(tt.input) - if len(got) != len(tt.want) { - t.Errorf("splitNoProxy(%q) = %v, want %v", tt.input, got, tt.want) - return - } - for i := range got { - if got[i] != tt.want[i] { - t.Errorf("splitNoProxy(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i]) - } - } - }) - } -} - func clearEnvVars(t *testing.T, keys []string) { t.Helper() for _, key := range keys { From b772b8fdad013a7e9f1e6761fb826406571887a8 Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Fri, 2 Jan 2026 09:58:43 +0000 Subject: [PATCH 8/9] refactor: skip proxy propagation for --help/--version and cleanup test --- cmd/claws/main.go | 4 ++-- cmd/claws/proxy_test.go | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/claws/main.go b/cmd/claws/main.go index 7b73215a..4b1a468b 100644 --- a/cmd/claws/main.go +++ b/cmd/claws/main.go @@ -19,10 +19,10 @@ import ( var version = "dev" func main() { - propagateAllProxy() - opts := parseFlags() + propagateAllProxy() + // Apply CLI options to global config cfg := config.Global() diff --git a/cmd/claws/proxy_test.go b/cmd/claws/proxy_test.go index ebe7c21a..6b667658 100644 --- a/cmd/claws/proxy_test.go +++ b/cmd/claws/proxy_test.go @@ -70,7 +70,6 @@ func TestPropagateAllProxy(t *testing.T) { func clearEnvVars(t *testing.T, keys []string) { t.Helper() for _, key := range keys { - t.Setenv(key, "") os.Unsetenv(key) } } From 6dce3ba72d5916e65da8c19f5161a2c3651b0ce1 Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Fri, 2 Jan 2026 10:11:41 +0000 Subject: [PATCH 9/9] test: add test case for lowercase all_proxy not supported --- cmd/claws/proxy_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cmd/claws/proxy_test.go b/cmd/claws/proxy_test.go index 6b667658..06cfb8e4 100644 --- a/cmd/claws/proxy_test.go +++ b/cmd/claws/proxy_test.go @@ -6,6 +6,12 @@ import ( ) func TestPropagateAllProxy(t *testing.T) { + // Note: Only uppercase ALL_PROXY is supported. + // Lowercase all_proxy is intentionally not supported to match + // the AWS SDK behavior and keep the implementation simple. + // Go's net/http.ProxyFromEnvironment also only checks uppercase + // for HTTP_PROXY and HTTPS_PROXY on non-CGI environments. + proxyVars := []string{ "ALL_PROXY", "HTTP_PROXY", @@ -48,6 +54,12 @@ func TestPropagateAllProxy(t *testing.T) { wantHTTPS: "", wantHTTP: "", }, + { + name: "lowercase all_proxy not supported", + envVars: map[string]string{"all_proxy": "http://proxy:8080"}, + wantHTTPS: "", + wantHTTP: "", + }, } for _, tt := range tests {