ContainerImage.Pinniped/internal/net/phttp/debug.go

119 lines
2.9 KiB
Go

// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package phttp
import (
"net/http"
"net/url"
"k8s.io/client-go/transport"
"go.pinniped.dev/internal/httputil/roundtripper"
)
func safeDebugWrappers(rt http.RoundTripper, f transport.WrapperFunc, shouldLog func() bool) http.RoundTripper {
return roundtripper.WrapFunc(rt, func(req *http.Request) (*http.Response, error) {
// minor optimization to avoid the cleaning logic when the debug wrappers are unused
// note: do not make this entire wrapper conditional on shouldLog() - the output is allowed to change at runtime
if !shouldLog() {
return rt.RoundTrip(req)
}
var (
resp *http.Response
err error
)
debugRT := f(roundtripper.Func(func(_ *http.Request) (*http.Response, error) {
// this call needs to be inside this closure so that the debug wrappers can time it
// note also that it takes the original (real) request
resp, err = rt.RoundTrip(req)
cleanedResp := cleanResp(resp) // do not leak the user's password during the password grant
return cleanedResp, err
}))
// run the debug wrappers for their side effects (i.e. logging)
// the output is ignored because the input is not the real request
cleanedReq := cleanReq(req) // do not leak the user's password during the password grant
_, _ = debugRT.RoundTrip(cleanedReq)
return resp, err
})
}
func cleanReq(req *http.Request) *http.Request {
// only pass back things we know to be safe to log
return &http.Request{
Method: req.Method,
URL: cleanURL(req.URL),
Header: cleanHeader(req.Header),
}
}
func cleanResp(resp *http.Response) *http.Response {
if resp == nil {
return nil
}
// only pass back things we know to be safe to log
return &http.Response{
Status: resp.Status,
Header: cleanHeader(resp.Header),
}
}
func cleanURL(u *url.URL) *url.URL {
var user *url.Userinfo
if len(u.User.Username()) > 0 {
user = url.User("masked_username")
}
var opaque string
if len(u.Opaque) > 0 {
opaque = "masked_opaque_data"
}
var fragment string
if len(u.Fragment) > 0 || len(u.RawFragment) > 0 {
fragment = "masked_fragment"
}
// only pass back things we know to be safe to log
return &url.URL{
Scheme: u.Scheme,
Opaque: opaque,
User: user,
Host: u.Host,
Path: u.Path,
RawPath: u.RawPath,
ForceQuery: u.ForceQuery,
RawQuery: cleanQuery(u.Query()),
Fragment: fragment,
}
}
func cleanQuery(query url.Values) string {
if len(query) == 0 {
return ""
}
out := url.Values(cleanHeader(http.Header(query))) // cast so we can re-use logic
return out.Encode()
}
func cleanHeader(header http.Header) http.Header {
if len(header) == 0 {
return nil
}
mask := []string{"masked_value"}
out := make(http.Header, len(header))
for key := range header {
out[key] = mask // only copy the keys
}
return out
}