package httpc import ( "bytes" "io" "net/http" "net/http/httputil" "strings" "time" "github.com/sirupsen/logrus" "git.wxccs.org/iceking2nd/winauth-go/internal/global" ) // Sensitive header names that must be redacted from Trace logs. Comparison // is case-insensitive. The body of responses is logged as-is at Trace level // — callers should not put bearer tokens in URL query strings if they want // to keep them out of logs. var sensitiveHeaders = map[string]struct{}{ "authorization": {}, "cookie": {}, "set-cookie": {}, "proxy-authorization": {}, "x-api-key": {}, "x-auth-token": {}, } // New returns an *http.Client that logs full request/response payloads at // Trace level. At lower levels it logs only a single Debug line with method, // URL, status code and duration. // // Sensitive headers (Authorization, Cookie, Set-Cookie, ...) are redacted // to "" before being written to the log, per the project rule // "never write secrets or tokens to logs". func New() *http.Client { return &http.Client{ Transport: &traceTransport{base: http.DefaultTransport}, Timeout: 30 * time.Second, } } type traceTransport struct { base http.RoundTripper } func (t *traceTransport) RoundTrip(req *http.Request) (*http.Response, error) { const fn = "internal.httpc.traceTransport.RoundTrip" logger := global.Log.WithField("func", fn).WithField("method", req.Method).WithField("url", req.URL.String()) trace := global.Log.GetLevel() >= logrus.TraceLevel if trace { dump, err := dumpRequest(req) if err != nil { logger.WithError(err).Trace("failed to dump request") } else { logger.WithField("payload", "request").Trace("\n" + dump) } } start := time.Now() resp, err := t.base.RoundTrip(req) dur := time.Since(start) if err != nil { logger.WithError(err).WithField("duration_ms", dur.Milliseconds()).Debug("http error") return nil, err } logger.WithField("status", resp.StatusCode).WithField("duration_ms", dur.Milliseconds()).Debug("http ok") if trace { dump, derr := dumpResponse(resp) if derr != nil { logger.WithError(derr).Trace("failed to dump response") } else { logger.WithField("payload", "response").Trace("\n" + dump) } } return resp, nil } // dumpRequest produces a textual dump of req with sensitive headers redacted. func dumpRequest(req *http.Request) (string, error) { clone := req.Clone(req.Context()) clone.Header = redactHeaders(req.Header) raw, err := httputil.DumpRequestOut(clone, true) if err != nil { return "", err } return string(raw), nil } func dumpResponse(resp *http.Response) (string, error) { if resp.Body != nil { body, err := io.ReadAll(resp.Body) if err != nil { return "", err } _ = resp.Body.Close() resp.Body = io.NopCloser(bytes.NewReader(body)) } origHeader := resp.Header resp.Header = redactHeaders(origHeader) defer func() { resp.Header = origHeader }() raw, err := httputil.DumpResponse(resp, true) if err != nil { return "", err } return string(raw), nil } func redactHeaders(h http.Header) http.Header { out := make(http.Header, len(h)) for k, v := range h { if _, ok := sensitiveHeaders[strings.ToLower(k)]; ok { out[k] = []string{""} continue } out[k] = append([]string(nil), v...) } return out }