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