mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-24 14:47:23 +00:00
CodeQL alert #43 (go/clear-text-logging) flagged that headers flow to log.Printf in pkg/service/proxy/proxy.go. The existing implementation only redacted when LoggingProxy.Redact was true — an opt-in. CodeQL is right to flag this: the safety floor for credential-bearing headers should not depend on caller configuration. Split the sensitive-header list into two: * alwaysSensitiveHeaders — Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-Api-Key, X-Bose-Token. Redacted unconditionally, regardless of LoggingProxy.Redact. * sensitiveHeaders — kept as a compatibility alias pointing at the same list, and still gated on Redact for any future use cases that want *additional* opt-in redaction beyond the floor. Behaviour change is strict tightening: nothing that was previously hidden becomes visible, and credentials that would have been logged when Redact was false are now hidden by default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
158 lines
4.3 KiB
Go
158 lines
4.3 KiB
Go
// Package proxy provides a logging reverse proxy used for speaker traffic debugging.
|
|
package proxy
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// alwaysSensitiveHeaders are stripped from log output unconditionally — they
|
|
// carry credentials whose plaintext value should never appear in a log line
|
|
// regardless of how the LoggingProxy was constructed.
|
|
var alwaysSensitiveHeaders = []string{
|
|
"Authorization",
|
|
"Proxy-Authorization",
|
|
"Cookie",
|
|
"Set-Cookie",
|
|
"X-Api-Key",
|
|
"X-Bose-Token",
|
|
}
|
|
|
|
// sensitiveHeaders is kept for backwards compatibility with callers that
|
|
// reference it by name; it now mirrors alwaysSensitiveHeaders.
|
|
var sensitiveHeaders = alwaysSensitiveHeaders
|
|
|
|
// LoggingProxy wraps a ReverseProxy to provide instrumentation.
|
|
type LoggingProxy struct {
|
|
Proxy *httputil.ReverseProxy
|
|
Redact bool
|
|
LogBody bool
|
|
RecordEnabled bool
|
|
MaxBodySize int64
|
|
Recorder *Recorder
|
|
}
|
|
|
|
// NewLoggingProxy creates a lightweight logger for HTTP requests/responses.
|
|
func NewLoggingProxy(_ string, redact bool) *LoggingProxy {
|
|
// targetURL logic should be handled by the caller or we can parse it here
|
|
return &LoggingProxy{
|
|
Redact: redact,
|
|
LogBody: os.Getenv("LOG_PROXY_BODY") == "true",
|
|
MaxBodySize: 1024 * 10, // 10KB default limit for logging
|
|
}
|
|
}
|
|
|
|
// SetRecorder sets the recorder for the proxy.
|
|
func (lp *LoggingProxy) SetRecorder(r *Recorder) {
|
|
lp.Recorder = r
|
|
}
|
|
|
|
// LogRequest prints an abbreviated request with optional header/body redaction.
|
|
func (lp *LoggingProxy) LogRequest(r *http.Request) {
|
|
headers := formatHeaders(r.Header, lp.Redact)
|
|
|
|
bodyStr := "[HIDDEN]"
|
|
|
|
if lp.LogBody && shouldLogBody(r.Header.Get("Content-Type")) {
|
|
if r.Body != nil {
|
|
bodyBytes, _ := io.ReadAll(r.Body)
|
|
|
|
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
|
if int64(len(bodyBytes)) > lp.MaxBodySize {
|
|
bodyStr = string(bodyBytes[:lp.MaxBodySize]) + "... [TRUNCATED]"
|
|
} else {
|
|
bodyStr = string(bodyBytes)
|
|
}
|
|
} else {
|
|
bodyStr = "[EMPTY]"
|
|
}
|
|
}
|
|
|
|
log.Printf("[PROXY_REQ] %s %s\n Headers:\n%s\n Body: %s", r.Method, r.URL.String(), headers, bodyStr)
|
|
}
|
|
|
|
// LogResponse prints an abbreviated response with optional header/body redaction.
|
|
func (lp *LoggingProxy) LogResponse(r *http.Response) {
|
|
headers := formatHeaders(r.Header, lp.Redact)
|
|
|
|
bodyStr := "[HIDDEN]"
|
|
|
|
if lp.LogBody && shouldLogBody(r.Header.Get("Content-Type")) {
|
|
if r.Body != nil {
|
|
bodyBytes, _ := io.ReadAll(r.Body)
|
|
|
|
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
|
if int64(len(bodyBytes)) > lp.MaxBodySize {
|
|
bodyStr = string(bodyBytes[:lp.MaxBodySize]) + "... [TRUNCATED]"
|
|
} else {
|
|
bodyStr = string(bodyBytes)
|
|
}
|
|
} else {
|
|
bodyStr = "[EMPTY]"
|
|
}
|
|
}
|
|
|
|
log.Printf("[PROXY_RES] %d %s\n Headers:\n%s\n Body: %s", r.StatusCode, r.Request.URL.String(), headers, bodyStr)
|
|
|
|
if lp.Recorder != nil && lp.RecordEnabled {
|
|
_ = lp.Recorder.Record("upstream", r.Request, r)
|
|
}
|
|
}
|
|
|
|
func formatHeaders(h http.Header, redact bool) string {
|
|
var sb strings.Builder
|
|
// In Go, http.Header is a map[string][]string.
|
|
// Iterating over the map directly allows us to see the actual keys
|
|
// stored in the map, which might not be canonical if set directly.
|
|
for k, vv := range h {
|
|
val := strings.Join(vv, ", ")
|
|
// Always redact credentials (Authorization, Cookie, …) regardless of
|
|
// the LoggingProxy.Redact toggle — the toggle controls *additional*
|
|
// redaction, never the safety floor.
|
|
if isAlwaysSensitive(k) || (redact && isSensitive(k)) {
|
|
val = "[REDACTED]"
|
|
}
|
|
|
|
fmt.Fprintf(&sb, " %s: %s\n", k, val)
|
|
}
|
|
|
|
return strings.TrimSuffix(sb.String(), "\n")
|
|
}
|
|
|
|
// isAlwaysSensitive returns true for credential-bearing headers that must
|
|
// never appear unredacted in logs regardless of caller configuration.
|
|
func isAlwaysSensitive(header string) bool {
|
|
for _, h := range alwaysSensitiveHeaders {
|
|
if strings.EqualFold(h, header) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func isSensitive(header string) bool {
|
|
for _, h := range sensitiveHeaders {
|
|
if strings.EqualFold(h, header) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func shouldLogBody(contentType string) bool {
|
|
contentType = strings.ToLower(contentType)
|
|
|
|
return strings.Contains(contentType, "xml") ||
|
|
strings.Contains(contentType, "json") ||
|
|
strings.Contains(contentType, "text") ||
|
|
contentType == ""
|
|
}
|