diff --git a/internal/cmdutil/secheader.go b/internal/cmdutil/secheader.go index 9b6fa83ea..cf99f9fd4 100644 --- a/internal/cmdutil/secheader.go +++ b/internal/cmdutil/secheader.go @@ -6,12 +6,10 @@ package cmdutil import ( "context" "net/http" - "os" "reflect" "runtime/debug" "strings" "sync" - "unicode" "github.com/larksuite/cli/extension/credential" "github.com/larksuite/cli/extension/fileio" @@ -40,8 +38,6 @@ const ( BuildKindUnknown = "unknown" officialModulePath = "github.com/larksuite/cli" - - agentTraceMaxLen = 1024 ) // UserAgentValue returns the User-Agent value: "lark-cli/{version}". @@ -49,25 +45,6 @@ func UserAgentValue() string { return SourceValue + "/" + build.Version } -// AgentTraceValue returns a header-safe value from the -// LARKSUITE_CLI_AGENT_TRACE environment variable. It trims -// surrounding whitespace, rejects values containing any Unicode -// control character or exceeding agentTraceMaxLen, and returns "" -// for any invalid or empty value. Callers can use the result -// directly in HTTP headers without further sanitisation. -func AgentTraceValue() string { - v := strings.TrimSpace(os.Getenv(envvars.CliAgentTrace)) - if v == "" || len(v) > agentTraceMaxLen { - return "" - } - for _, r := range v { - if unicode.IsControl(r) { - return "" - } - } - return v -} - // BaseSecurityHeaders returns headers that every request must carry. func BaseSecurityHeaders() http.Header { h := make(http.Header) @@ -75,7 +52,7 @@ func BaseSecurityHeaders() http.Header { h.Set(HeaderVersion, build.Version) h.Set(HeaderBuild, DetectBuildKind()) h.Set(HeaderUserAgent, UserAgentValue()) - if v := AgentTraceValue(); v != "" { + if v := envvars.AgentTrace(); v != "" { h.Set(HeaderAgentTrace, v) } return h diff --git a/internal/cmdutil/secheader_test.go b/internal/cmdutil/secheader_test.go index 6b116f458..b206d8171 100644 --- a/internal/cmdutil/secheader_test.go +++ b/internal/cmdutil/secheader_test.go @@ -6,7 +6,6 @@ package cmdutil import ( "context" "net/http" - "strings" "testing" "github.com/larksuite/cli/extension/credential" @@ -264,88 +263,9 @@ func TestBaseSecurityHeaders_AllRequiredHeaders(t *testing.T) { } // --------------------------------------------------------------------------- -// AgentTraceValue / HeaderAgentTrace +// HeaderAgentTrace injection (via BaseSecurityHeaders) // --------------------------------------------------------------------------- -func TestAgentTraceValue_EmptyWhenEnvUnset(t *testing.T) { - t.Setenv(envvars.CliAgentTrace, "") - if got := AgentTraceValue(); got != "" { - t.Fatalf("AgentTraceValue() = %q, want empty when env unset", got) - } -} - -func TestAgentTraceValue_ReturnsCleanValue(t *testing.T) { - t.Setenv(envvars.CliAgentTrace, "trace-abc-123") - if got := AgentTraceValue(); got != "trace-abc-123" { - t.Fatalf("AgentTraceValue() = %q, want %q", got, "trace-abc-123") - } -} - -func TestAgentTraceValue_TrimsWhitespace(t *testing.T) { - t.Setenv(envvars.CliAgentTrace, " trace-trim ") - if got := AgentTraceValue(); got != "trace-trim" { - t.Fatalf("AgentTraceValue() = %q, want %q (whitespace trimmed)", got, "trace-trim") - } -} - -func TestAgentTraceValue_OnlyWhitespace_ReturnsEmpty(t *testing.T) { - t.Setenv(envvars.CliAgentTrace, " ") - if got := AgentTraceValue(); got != "" { - t.Fatalf("AgentTraceValue() = %q, want empty for whitespace-only value", got) - } -} - -func TestAgentTraceValue_RejectsCRLF(t *testing.T) { - t.Setenv(envvars.CliAgentTrace, "val\r\nX-Evil: attack") - if got := AgentTraceValue(); got != "" { - t.Fatalf("AgentTraceValue() = %q, want empty for CR/LF value", got) - } -} - -func TestAgentTraceValue_RejectsLF(t *testing.T) { - t.Setenv(envvars.CliAgentTrace, "val\nX-Evil: attack") - if got := AgentTraceValue(); got != "" { - t.Fatalf("AgentTraceValue() = %q, want empty for LF value", got) - } -} - -func TestAgentTraceValue_RejectsTab(t *testing.T) { - t.Setenv(envvars.CliAgentTrace, "val\tinjected") - if got := AgentTraceValue(); got != "" { - t.Fatalf("AgentTraceValue() = %q, want empty for tab value", got) - } -} - -func TestAgentTraceValue_RejectsControlChar(t *testing.T) { - t.Setenv(envvars.CliAgentTrace, "val\x01injected") - if got := AgentTraceValue(); got != "" { - t.Fatalf("AgentTraceValue() = %q, want empty for control char value", got) - } -} - -func TestAgentTraceValue_RejectsDEL(t *testing.T) { - t.Setenv(envvars.CliAgentTrace, "val\x7finjected") - if got := AgentTraceValue(); got != "" { - t.Fatalf("AgentTraceValue() = %q, want empty for DEL value", got) - } -} - -func TestAgentTraceValue_RejectsOverlongValue(t *testing.T) { - longVal := strings.Repeat("a", agentTraceMaxLen+1) - t.Setenv(envvars.CliAgentTrace, longVal) - if got := AgentTraceValue(); got != "" { - t.Fatalf("AgentTraceValue() returned non-empty for %d-byte value (max %d)", len(longVal), agentTraceMaxLen) - } -} - -func TestAgentTraceValue_AcceptsMaxLengthValue(t *testing.T) { - val := strings.Repeat("a", agentTraceMaxLen) - t.Setenv(envvars.CliAgentTrace, val) - if got := AgentTraceValue(); got != val { - t.Fatalf("AgentTraceValue() = %q, want %d-byte value accepted", got, agentTraceMaxLen) - } -} - func TestBaseSecurityHeaders_NoAgentTraceHeaderWhenEnvUnset(t *testing.T) { t.Setenv(envvars.CliAgentTrace, "") h := BaseSecurityHeaders() diff --git a/internal/envvars/read.go b/internal/envvars/read.go index ca767e917..34868386c 100644 --- a/internal/envvars/read.go +++ b/internal/envvars/read.go @@ -9,12 +9,19 @@ import ( "unicode" ) -const agentNameMaxLen = 128 +const ( + agentNameMaxLen = 128 + agentTraceMaxLen = 1024 +) func AgentName() string { return sanitizeSingleLine(os.Getenv(CliAgentName), agentNameMaxLen) } +func AgentTrace() string { + return sanitizeSingleLine(os.Getenv(CliAgentTrace), agentTraceMaxLen) +} + func sanitizeSingleLine(raw string, maxLen int) string { v := strings.TrimSpace(raw) if v == "" || len(v) > maxLen { diff --git a/internal/envvars/read_test.go b/internal/envvars/read_test.go index 3ab3cd0f8..39903e17b 100644 --- a/internal/envvars/read_test.go +++ b/internal/envvars/read_test.go @@ -50,3 +50,82 @@ func TestAgentName_RejectsOverlongValue(t *testing.T) { t.Fatalf("AgentName() returned non-empty for %d-byte value (max %d)", len(longVal), agentNameMaxLen) } } + +func TestAgentTrace_EmptyWhenEnvUnset(t *testing.T) { + t.Setenv(CliAgentTrace, "") + if got := AgentTrace(); got != "" { + t.Fatalf("AgentTrace() = %q, want empty when env unset", got) + } +} + +func TestAgentTrace_ReturnsCleanValue(t *testing.T) { + t.Setenv(CliAgentTrace, "trace-abc-123") + if got := AgentTrace(); got != "trace-abc-123" { + t.Fatalf("AgentTrace() = %q, want %q", got, "trace-abc-123") + } +} + +func TestAgentTrace_TrimsWhitespace(t *testing.T) { + t.Setenv(CliAgentTrace, " trace-trim ") + if got := AgentTrace(); got != "trace-trim" { + t.Fatalf("AgentTrace() = %q, want %q (whitespace trimmed)", got, "trace-trim") + } +} + +func TestAgentTrace_OnlyWhitespace_ReturnsEmpty(t *testing.T) { + t.Setenv(CliAgentTrace, " ") + if got := AgentTrace(); got != "" { + t.Fatalf("AgentTrace() = %q, want empty for whitespace-only value", got) + } +} + +func TestAgentTrace_RejectsCRLF(t *testing.T) { + t.Setenv(CliAgentTrace, "val\r\nX-Evil: attack") + if got := AgentTrace(); got != "" { + t.Fatalf("AgentTrace() = %q, want empty for CR/LF value", got) + } +} + +func TestAgentTrace_RejectsLF(t *testing.T) { + t.Setenv(CliAgentTrace, "val\nX-Evil: attack") + if got := AgentTrace(); got != "" { + t.Fatalf("AgentTrace() = %q, want empty for LF value", got) + } +} + +func TestAgentTrace_RejectsTab(t *testing.T) { + t.Setenv(CliAgentTrace, "val\tinjected") + if got := AgentTrace(); got != "" { + t.Fatalf("AgentTrace() = %q, want empty for tab value", got) + } +} + +func TestAgentTrace_RejectsControlChar(t *testing.T) { + t.Setenv(CliAgentTrace, "val\x01injected") + if got := AgentTrace(); got != "" { + t.Fatalf("AgentTrace() = %q, want empty for control char value", got) + } +} + +func TestAgentTrace_RejectsDEL(t *testing.T) { + t.Setenv(CliAgentTrace, "val\x7finjected") + if got := AgentTrace(); got != "" { + t.Fatalf("AgentTrace() = %q, want empty for DEL value", got) + } +} + +func TestAgentTrace_RejectsOverlongValue(t *testing.T) { + longVal := strings.Repeat("a", agentTraceMaxLen+1) + t.Setenv(CliAgentTrace, longVal) + if got := AgentTrace(); got != "" { + t.Fatalf("AgentTrace() returned non-empty for %d-byte value (max %d)", len(longVal), agentTraceMaxLen) + } +} + +func TestAgentTrace_AcceptsMaxLengthValue(t *testing.T) { + val := strings.Repeat("a", agentTraceMaxLen) + t.Setenv(CliAgentTrace, val) + if got := AgentTrace(); got != val { + t.Fatalf("AgentTrace() = %q, want %d-byte value accepted", got, agentTraceMaxLen) + } +}