client.go raw
1 package clientdebug
2
3 import (
4 "fmt"
5 "io"
6 "net/http"
7 "net/http/httputil"
8 "os"
9 "regexp"
10 "strconv"
11 "strings"
12
13 "github.com/go-acme/lego/v4/platform/config/env"
14 )
15
16 const replacement = "***"
17
18 type Option func(*DumpTransport)
19
20 func WithEnvKeys(keys ...string) Option {
21 return func(d *DumpTransport) {
22 for _, key := range keys {
23 v := strings.TrimSpace(env.GetOrFile(key))
24 if v == "" {
25 continue
26 }
27
28 d.replacements = append(d.replacements, v, replacement)
29 }
30 }
31 }
32
33 func WithValues(values ...string) Option {
34 return func(d *DumpTransport) {
35 for _, value := range values {
36 d.replacements = append(d.replacements, value, replacement)
37 }
38 }
39 }
40
41 func WithHeaders(keys ...string) Option {
42 return func(d *DumpTransport) {
43 d.regexps = append(d.regexps,
44 regexp.MustCompile(fmt.Sprintf(`(?im)^(%s):.+$`, strings.Join(keys, "|"))))
45 }
46 }
47
48 type DumpTransport struct {
49 rt http.RoundTripper
50
51 replacements []string
52 replacer *strings.Replacer
53
54 regexps []*regexp.Regexp
55
56 writer io.Writer
57 }
58
59 func NewDumpTransport(rt http.RoundTripper, opts ...Option) *DumpTransport {
60 if rt == nil {
61 rt = http.DefaultTransport
62 }
63
64 d := &DumpTransport{
65 rt: rt,
66 writer: os.Stdout,
67 }
68
69 for _, opt := range opts {
70 opt(d)
71 }
72
73 d.regexps = append(d.regexps,
74 regexp.MustCompile(`(?im)^(Authorization):.+$`),
75 regexp.MustCompile(`(?im)^(Token|X-Token):.+$`),
76 regexp.MustCompile(`(?im)^(Auth-Token|X-Auth-Token):.+$`),
77 regexp.MustCompile(`(?im)^(Api-Key|X-Api-Key|X-Api-Secret):.+$`),
78 )
79
80 if len(d.replacements) > 0 {
81 d.replacer = strings.NewReplacer(d.replacements...)
82 }
83
84 return d
85 }
86
87 func (d *DumpTransport) RoundTrip(h *http.Request) (*http.Response, error) {
88 data, _ := httputil.DumpRequestOut(h, true)
89
90 _, _ = fmt.Fprintln(d.writer, "[HTTP Request]")
91 _, _ = fmt.Fprintln(d.writer, d.redact(data))
92
93 resp, err := d.rt.RoundTrip(h)
94 if err != nil {
95 return nil, err
96 }
97
98 data, _ = httputil.DumpResponse(resp, true)
99
100 _, _ = fmt.Fprintln(d.writer, "[HTTP Response]")
101 _, _ = fmt.Fprintln(d.writer, d.redact(data))
102
103 return resp, err
104 }
105
106 func (d *DumpTransport) redact(content []byte) string {
107 data := string(content)
108
109 for _, r := range d.regexps {
110 data = r.ReplaceAllString(data, "$1: "+replacement)
111 }
112
113 if d.replacer == nil {
114 return data
115 }
116
117 return d.replacer.Replace(data)
118 }
119
120 // Wrap wraps an HTTP client Transport with the [DumpTransport].
121 func Wrap(client *http.Client, opts ...Option) *http.Client {
122 val, found := os.LookupEnv("LEGO_DEBUG_DNS_API_HTTP_CLIENT")
123 if !found {
124 return client
125 }
126
127 if ok, _ := strconv.ParseBool(val); !ok {
128 return client
129 }
130
131 client.Transport = NewDumpTransport(client.Transport, opts...)
132
133 return client
134 }
135