119 lines
2.9 KiB
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
|
||
|
}
|