mirror of
https://github.com/larksuite/cli.git
synced 2026-07-06 00:06:28 +08:00
internal/util imported internal/proxyplugin (SharedTransport, FallbackTransport, NewHTTPClient, and WarnIfProxied via proxyPluginStatus), so a foundational util package depended up into a feature package, pulling binding/core/vfs into the transitive cone of every util importer. Move internal/proxyplugin -> internal/transport and make it the single owner of outbound transport: fold the two SharedTransport functions into one Shared() (proxy-plugin override -> LARK_CLI_NO_PROXY -> http.DefaultTransport), and move Fallback/NewHTTPClient/WarnIfProxied/DetectProxyEnv/noProxyTransport out of the now-deleted internal/util/proxy.go into the new package. The proxy-plugin probe is demoted to a private pluginTransport(); the duplicate redactProxyURL collapses to one. internal/util keeps no proxy code and is a leaf again. Re-point all consumers (registry, doctor, config, auth, cmdutil, update) to internal/transport. Behavior-preserving: package move + symbol rename + dedup. Two new tests lock the fail-closed contract (plugin overrides NO_PROXY; malformed config never falls through to direct egress).
259 lines
7.9 KiB
Go
259 lines
7.9 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package transport
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/internal/envvars"
|
|
)
|
|
|
|
// TestDetectProxyEnv verifies proxy environment detection priority and empty-state behavior.
|
|
func TestDetectProxyEnv(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetProxyPluginEnv(t)
|
|
|
|
// Clear all proxy env vars first
|
|
for _, k := range proxyEnvKeys {
|
|
t.Setenv(k, "")
|
|
}
|
|
|
|
key, val := DetectProxyEnv()
|
|
if key != "" || val != "" {
|
|
t.Errorf("expected no proxy, got %s=%s", key, val)
|
|
}
|
|
|
|
t.Setenv("HTTPS_PROXY", "http://proxy:8888")
|
|
key, val = DetectProxyEnv()
|
|
if key != "HTTPS_PROXY" || val != "http://proxy:8888" {
|
|
t.Errorf("expected HTTPS_PROXY=http://proxy:8888, got %s=%s", key, val)
|
|
}
|
|
}
|
|
|
|
// TestWarnIfProxied_WithProxy verifies that proxy detection emits a warning.
|
|
func TestWarnIfProxied_WithProxy(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetProxyPluginEnv(t)
|
|
resetProxyPluginState()
|
|
proxyWarningOnce = sync.Once{}
|
|
|
|
t.Setenv("HTTPS_PROXY", "http://corp-proxy:3128")
|
|
|
|
var buf bytes.Buffer
|
|
WarnIfProxied(&buf)
|
|
|
|
out := buf.String()
|
|
if out == "" {
|
|
t.Error("expected warning output when proxy is set")
|
|
}
|
|
if !bytes.Contains([]byte(out), []byte("HTTPS_PROXY")) {
|
|
t.Errorf("warning should mention HTTPS_PROXY, got: %s", out)
|
|
}
|
|
if !bytes.Contains([]byte(out), []byte(EnvNoProxy)) {
|
|
t.Errorf("warning should mention %s, got: %s", EnvNoProxy, out)
|
|
}
|
|
}
|
|
|
|
// TestWarnIfProxied_WithoutProxy verifies that no warning is emitted without proxy settings.
|
|
func TestWarnIfProxied_WithoutProxy(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetProxyPluginEnv(t)
|
|
resetProxyPluginState()
|
|
proxyWarningOnce = sync.Once{}
|
|
|
|
for _, k := range proxyEnvKeys {
|
|
t.Setenv(k, "")
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
WarnIfProxied(&buf)
|
|
|
|
if buf.Len() != 0 {
|
|
t.Errorf("expected no output when no proxy is set, got: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
// TestWarnIfProxied_SilentWhenDisabled verifies that LARK_CLI_NO_PROXY suppresses warnings.
|
|
func TestWarnIfProxied_SilentWhenDisabled(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetProxyPluginEnv(t)
|
|
resetProxyPluginState()
|
|
proxyWarningOnce = sync.Once{}
|
|
|
|
t.Setenv("HTTPS_PROXY", "http://proxy:8080")
|
|
t.Setenv(EnvNoProxy, "1")
|
|
|
|
var buf bytes.Buffer
|
|
WarnIfProxied(&buf)
|
|
|
|
if buf.Len() != 0 {
|
|
t.Errorf("expected no warning when proxy is disabled, got: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
// TestWarnIfProxied_OnlyOnce verifies that proxy warnings are emitted only once.
|
|
func TestWarnIfProxied_OnlyOnce(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetProxyPluginEnv(t)
|
|
resetProxyPluginState()
|
|
proxyWarningOnce = sync.Once{}
|
|
|
|
t.Setenv("HTTP_PROXY", "http://proxy:1234")
|
|
|
|
var buf bytes.Buffer
|
|
WarnIfProxied(&buf)
|
|
first := buf.String()
|
|
|
|
WarnIfProxied(&buf)
|
|
second := buf.String()
|
|
|
|
if first == "" {
|
|
t.Error("expected warning on first call")
|
|
}
|
|
if second != first {
|
|
t.Error("expected no additional output on second call")
|
|
}
|
|
}
|
|
|
|
// TestWarnIfProxied_ProxyPluginEnabled verifies that when proxy plugin mode is
|
|
// enabled, the warning describes the plugin proxy and the correct disable method
|
|
// (LARKSUITE_CLI_PROXY_ENABLE=false) instead of the misleading LARK_CLI_NO_PROXY
|
|
// instruction — even when env proxy and LARK_CLI_NO_PROXY are also set.
|
|
func TestWarnIfProxied_ProxyPluginEnabled(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetProxyPluginEnv(t)
|
|
proxyWarningOnce = sync.Once{}
|
|
|
|
old := proxyPluginStatus
|
|
proxyPluginStatus = func() (string, string, bool) { return "http://127.0.0.1:3128", "", true }
|
|
t.Cleanup(func() { proxyPluginStatus = old })
|
|
|
|
// Plugin mode overrides these; the warning must still be the plugin one.
|
|
t.Setenv("HTTPS_PROXY", "http://corp-proxy:8080")
|
|
t.Setenv(EnvNoProxy, "1")
|
|
|
|
var buf bytes.Buffer
|
|
WarnIfProxied(&buf)
|
|
out := buf.String()
|
|
|
|
if !strings.Contains(out, "127.0.0.1:3128") {
|
|
t.Errorf("warning should mention the plugin proxy address, got: %s", out)
|
|
}
|
|
if !strings.Contains(out, envvars.CliProxyEnable) {
|
|
t.Errorf("warning should mention %s as the disable method, got: %s", envvars.CliProxyEnable, out)
|
|
}
|
|
if strings.Contains(out, "Set "+EnvNoProxy+"=1") {
|
|
t.Errorf("warning must NOT give the misleading %s disable instruction when plugin is enabled, got: %s", EnvNoProxy, out)
|
|
}
|
|
// No custom CA configured -> no interception warning.
|
|
if strings.Contains(out, "custom CA") {
|
|
t.Errorf("warning should not mention a custom CA when none is configured, got: %s", out)
|
|
}
|
|
}
|
|
|
|
// TestWarnIfProxied_ProxyPluginCustomCAWarns verifies that when a custom CA is
|
|
// trusted, the warning surfaces the TLS-interception capability.
|
|
func TestWarnIfProxied_ProxyPluginCustomCAWarns(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetProxyPluginEnv(t)
|
|
proxyWarningOnce = sync.Once{}
|
|
|
|
old := proxyPluginStatus
|
|
proxyPluginStatus = func() (string, string, bool) {
|
|
return "http://127.0.0.1:3128", "/etc/lark/extra_ca.pem", true
|
|
}
|
|
t.Cleanup(func() { proxyPluginStatus = old })
|
|
|
|
var buf bytes.Buffer
|
|
WarnIfProxied(&buf)
|
|
out := buf.String()
|
|
|
|
if !strings.Contains(out, "custom CA") {
|
|
t.Errorf("warning should mention the custom CA, got: %s", out)
|
|
}
|
|
if !strings.Contains(out, "/etc/lark/extra_ca.pem") {
|
|
t.Errorf("warning should include the CA path, got: %s", out)
|
|
}
|
|
if !strings.Contains(out, "intercept") {
|
|
t.Errorf("warning should mention TLS interception, got: %s", out)
|
|
}
|
|
}
|
|
|
|
// TestWarnIfProxied_ProxyPluginEnabledRedactsCredentials verifies the plugin
|
|
// warning never leaks credentials embedded in the configured proxy address.
|
|
func TestWarnIfProxied_ProxyPluginEnabledRedactsCredentials(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetProxyPluginEnv(t)
|
|
proxyWarningOnce = sync.Once{}
|
|
|
|
old := proxyPluginStatus
|
|
proxyPluginStatus = func() (string, string, bool) { return "http://user:s3cret@127.0.0.1:3128", "", true }
|
|
t.Cleanup(func() { proxyPluginStatus = old })
|
|
|
|
var buf bytes.Buffer
|
|
WarnIfProxied(&buf)
|
|
out := buf.String()
|
|
|
|
if strings.Contains(out, "s3cret") {
|
|
t.Errorf("plugin warning leaked password, got: %s", out)
|
|
}
|
|
if strings.Contains(out, "user:") {
|
|
t.Errorf("plugin warning leaked username, got: %s", out)
|
|
}
|
|
if !strings.Contains(out, "***@127.0.0.1:3128") {
|
|
t.Errorf("plugin warning should contain redacted proxy URL, got: %s", out)
|
|
}
|
|
}
|
|
|
|
// TestRedactProxyURL verifies redaction of proxy credentials across supported formats.
|
|
func TestRedactProxyURL(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want string
|
|
}{
|
|
{"http://proxy:8080", "http://proxy:8080"},
|
|
{"http://user:pass@proxy:8080", "http://***@proxy:8080/"},
|
|
{"http://user:p%40ss@proxy:8080/path", "http://***@proxy:8080/path"},
|
|
{"http://user@proxy:8080", "http://***@proxy:8080/"},
|
|
{"socks5://admin:secret@10.0.0.1:1080", "socks5://***@10.0.0.1:1080/"},
|
|
{"user:pass@proxy:8080", "***@proxy:8080"},
|
|
{"admin:s3cret@10.0.0.1:3128", "***@10.0.0.1:3128"},
|
|
{"not-a-url", "not-a-url"},
|
|
{"", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
got := redactProxyURL(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("redactProxyURL(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestWarnIfProxied_RedactsCredentials verifies that warning output never leaks credentials.
|
|
func TestWarnIfProxied_RedactsCredentials(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
unsetProxyPluginEnv(t)
|
|
resetProxyPluginState()
|
|
proxyWarningOnce = sync.Once{}
|
|
|
|
t.Setenv("HTTPS_PROXY", "http://admin:s3cret@proxy:8080")
|
|
|
|
var buf bytes.Buffer
|
|
WarnIfProxied(&buf)
|
|
|
|
out := buf.String()
|
|
if bytes.Contains([]byte(out), []byte("s3cret")) {
|
|
t.Errorf("warning should not contain proxy password, got: %s", out)
|
|
}
|
|
if bytes.Contains([]byte(out), []byte("admin")) {
|
|
t.Errorf("warning should not contain proxy username, got: %s", out)
|
|
}
|
|
if !bytes.Contains([]byte(out), []byte("***@proxy:8080")) {
|
|
t.Errorf("warning should contain redacted proxy URL, got: %s", out)
|
|
}
|
|
}
|