sender.go raw

   1  package sender
   2  
   3  import (
   4  	"encoding/json"
   5  	"fmt"
   6  	"io"
   7  	"net/http"
   8  	"runtime"
   9  	"strings"
  10  
  11  	"github.com/go-acme/lego/v4/acme"
  12  )
  13  
  14  type RequestOption func(*http.Request) error
  15  
  16  func contentType(ct string) RequestOption {
  17  	return func(req *http.Request) error {
  18  		req.Header.Set("Content-Type", ct)
  19  		return nil
  20  	}
  21  }
  22  
  23  type Doer struct {
  24  	httpClient *http.Client
  25  	userAgent  string
  26  }
  27  
  28  // NewDoer Creates a new Doer.
  29  func NewDoer(client *http.Client, userAgent string) *Doer {
  30  	client.Transport = newHTTPSOnly(client)
  31  
  32  	return &Doer{
  33  		httpClient: client,
  34  		userAgent:  userAgent,
  35  	}
  36  }
  37  
  38  // Get performs a GET request with a proper User-Agent string.
  39  // If "response" is not provided, callers should close resp.Body when done reading from it.
  40  func (d *Doer) Get(url string, response any) (*http.Response, error) {
  41  	req, err := d.newRequest(http.MethodGet, url, nil)
  42  	if err != nil {
  43  		return nil, err
  44  	}
  45  
  46  	return d.do(req, response)
  47  }
  48  
  49  // Head performs a HEAD request with a proper User-Agent string.
  50  // The response body (resp.Body) is already closed when this function returns.
  51  func (d *Doer) Head(url string) (*http.Response, error) {
  52  	req, err := d.newRequest(http.MethodHead, url, nil)
  53  	if err != nil {
  54  		return nil, err
  55  	}
  56  
  57  	return d.do(req, nil)
  58  }
  59  
  60  // Post performs a POST request with a proper User-Agent string.
  61  // If "response" is not provided, callers should close resp.Body when done reading from it.
  62  func (d *Doer) Post(url string, body io.Reader, bodyType string, response any) (*http.Response, error) {
  63  	req, err := d.newRequest(http.MethodPost, url, body, contentType(bodyType))
  64  	if err != nil {
  65  		return nil, err
  66  	}
  67  
  68  	return d.do(req, response)
  69  }
  70  
  71  func (d *Doer) newRequest(method, uri string, body io.Reader, opts ...RequestOption) (*http.Request, error) {
  72  	req, err := http.NewRequest(method, uri, body)
  73  	if err != nil {
  74  		return nil, fmt.Errorf("failed to create request: %w", err)
  75  	}
  76  
  77  	req.Header.Set("User-Agent", d.formatUserAgent())
  78  
  79  	for _, opt := range opts {
  80  		err = opt(req)
  81  		if err != nil {
  82  			return nil, fmt.Errorf("failed to create request: %w", err)
  83  		}
  84  	}
  85  
  86  	return req, nil
  87  }
  88  
  89  func (d *Doer) do(req *http.Request, response any) (*http.Response, error) {
  90  	resp, err := d.httpClient.Do(req)
  91  	if err != nil {
  92  		return nil, err
  93  	}
  94  
  95  	if err = checkError(req, resp); err != nil {
  96  		return resp, err
  97  	}
  98  
  99  	if response != nil {
 100  		raw, err := io.ReadAll(resp.Body)
 101  		if err != nil {
 102  			return resp, err
 103  		}
 104  
 105  		defer resp.Body.Close()
 106  
 107  		err = json.Unmarshal(raw, response)
 108  		if err != nil {
 109  			return resp, fmt.Errorf("failed to unmarshal %q to type %T: %w", raw, response, err)
 110  		}
 111  	}
 112  
 113  	return resp, nil
 114  }
 115  
 116  // formatUserAgent builds and returns the User-Agent string to use in requests.
 117  func (d *Doer) formatUserAgent() string {
 118  	ua := fmt.Sprintf("%s %s (%s; %s; %s)", d.userAgent, ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH)
 119  	return strings.TrimSpace(ua)
 120  }
 121  
 122  func checkError(req *http.Request, resp *http.Response) error {
 123  	if resp.StatusCode < http.StatusBadRequest {
 124  		return nil
 125  	}
 126  
 127  	body, err := io.ReadAll(resp.Body)
 128  	if err != nil {
 129  		return fmt.Errorf("%d :: %s :: %s :: %w", resp.StatusCode, req.Method, req.URL, err)
 130  	}
 131  
 132  	var errorDetails *acme.ProblemDetails
 133  
 134  	err = json.Unmarshal(body, &errorDetails)
 135  	if err != nil {
 136  		return fmt.Errorf("%d ::%s :: %s :: %w :: %s", resp.StatusCode, req.Method, req.URL, err, string(body))
 137  	}
 138  
 139  	errorDetails.Method = req.Method
 140  	errorDetails.URL = req.URL.String()
 141  
 142  	if errorDetails.HTTPStatus == 0 {
 143  		errorDetails.HTTPStatus = resp.StatusCode
 144  	}
 145  
 146  	// Check for errors we handle specifically
 147  	switch {
 148  	case errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr:
 149  		return &acme.NonceError{ProblemDetails: errorDetails}
 150  
 151  	case errorDetails.HTTPStatus == http.StatusConflict && errorDetails.Type == acme.AlreadyReplacedErr:
 152  		return &acme.AlreadyReplacedError{ProblemDetails: errorDetails}
 153  
 154  	case errorDetails.HTTPStatus == http.StatusTooManyRequests && errorDetails.Type == acme.RateLimitedErr:
 155  		return &acme.RateLimitedError{
 156  			ProblemDetails: errorDetails,
 157  			RetryAfter:     resp.Header.Get("Retry-After"),
 158  		}
 159  
 160  	default:
 161  		return errorDetails
 162  	}
 163  }
 164  
 165  type httpsOnly struct {
 166  	rt http.RoundTripper
 167  }
 168  
 169  func newHTTPSOnly(client *http.Client) *httpsOnly {
 170  	if client.Transport == nil {
 171  		return &httpsOnly{rt: http.DefaultTransport}
 172  	}
 173  
 174  	return &httpsOnly{rt: client.Transport}
 175  }
 176  
 177  // RoundTrip ensure HTTPS is used.
 178  // Each ACME function is accomplished by the client sending a sequence of HTTPS requests to the server [RFC2818],
 179  // carrying JSON messages [RFC8259].
 180  // Use of HTTPS is REQUIRED.
 181  // https://datatracker.ietf.org/doc/html/rfc8555#section-6.1
 182  func (r *httpsOnly) RoundTrip(req *http.Request) (*http.Response, error) {
 183  	if req.URL.Scheme != "https" {
 184  		return nil, fmt.Errorf("HTTPS is required: %s", req.URL)
 185  	}
 186  
 187  	return r.rt.RoundTrip(req)
 188  }
 189