2019-03-11 16:56:48 +00:00
|
|
|
package sender
|
2018-12-06 21:50:17 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"runtime"
|
|
|
|
"strings"
|
|
|
|
|
2020-09-02 01:20:01 +00:00
|
|
|
"github.com/go-acme/lego/v4/acme"
|
2018-12-06 21:50:17 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type RequestOption func(*http.Request) error
|
|
|
|
|
|
|
|
func contentType(ct string) RequestOption {
|
|
|
|
return func(req *http.Request) error {
|
|
|
|
req.Header.Set("Content-Type", ct)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type Doer struct {
|
|
|
|
httpClient *http.Client
|
|
|
|
userAgent string
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewDoer Creates a new Doer.
|
|
|
|
func NewDoer(client *http.Client, userAgent string) *Doer {
|
|
|
|
return &Doer{
|
|
|
|
httpClient: client,
|
|
|
|
userAgent: userAgent,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get performs a GET request with a proper User-Agent string.
|
|
|
|
// If "response" is not provided, callers should close resp.Body when done reading from it.
|
|
|
|
func (d *Doer) Get(url string, response interface{}) (*http.Response, error) {
|
|
|
|
req, err := d.newRequest(http.MethodGet, url, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.do(req, response)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Head performs a HEAD request with a proper User-Agent string.
|
|
|
|
// The response body (resp.Body) is already closed when this function returns.
|
|
|
|
func (d *Doer) Head(url string) (*http.Response, error) {
|
|
|
|
req, err := d.newRequest(http.MethodHead, url, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.do(req, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Post performs a POST request with a proper User-Agent string.
|
|
|
|
// If "response" is not provided, callers should close resp.Body when done reading from it.
|
|
|
|
func (d *Doer) Post(url string, body io.Reader, bodyType string, response interface{}) (*http.Response, error) {
|
|
|
|
req, err := d.newRequest(http.MethodPost, url, body, contentType(bodyType))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.do(req, response)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Doer) newRequest(method, uri string, body io.Reader, opts ...RequestOption) (*http.Request, error) {
|
|
|
|
req, err := http.NewRequest(method, uri, body)
|
|
|
|
if err != nil {
|
2020-02-27 18:14:46 +00:00
|
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
2018-12-06 21:50:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("User-Agent", d.formatUserAgent())
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
err = opt(req)
|
|
|
|
if err != nil {
|
2020-02-27 18:14:46 +00:00
|
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
2018-12-06 21:50:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Doer) do(req *http.Request, response interface{}) (*http.Response, error) {
|
|
|
|
resp, err := d.httpClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = checkError(req, resp); err != nil {
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if response != nil {
|
2021-08-25 09:44:11 +00:00
|
|
|
raw, err := io.ReadAll(resp.Body)
|
2018-12-06 21:50:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
err = json.Unmarshal(raw, response)
|
|
|
|
if err != nil {
|
2020-02-27 18:14:46 +00:00
|
|
|
return resp, fmt.Errorf("failed to unmarshal %q to type %T: %w", raw, response, err)
|
2018-12-06 21:50:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// formatUserAgent builds and returns the User-Agent string to use in requests.
|
|
|
|
func (d *Doer) formatUserAgent() string {
|
|
|
|
ua := fmt.Sprintf("%s %s (%s; %s; %s)", d.userAgent, ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH)
|
|
|
|
return strings.TrimSpace(ua)
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkError(req *http.Request, resp *http.Response) error {
|
|
|
|
if resp.StatusCode >= http.StatusBadRequest {
|
2021-08-25 09:44:11 +00:00
|
|
|
body, err := io.ReadAll(resp.Body)
|
2018-12-06 21:50:17 +00:00
|
|
|
if err != nil {
|
2020-02-27 18:14:46 +00:00
|
|
|
return fmt.Errorf("%d :: %s :: %s :: %w", resp.StatusCode, req.Method, req.URL, err)
|
2018-12-06 21:50:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var errorDetails *acme.ProblemDetails
|
|
|
|
err = json.Unmarshal(body, &errorDetails)
|
|
|
|
if err != nil {
|
2020-02-27 18:14:46 +00:00
|
|
|
return fmt.Errorf("%d ::%s :: %s :: %w :: %s", resp.StatusCode, req.Method, req.URL, err, string(body))
|
2018-12-06 21:50:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
errorDetails.Method = req.Method
|
|
|
|
errorDetails.URL = req.URL.String()
|
|
|
|
|
2023-06-19 15:02:21 +00:00
|
|
|
if errorDetails.HTTPStatus == 0 {
|
|
|
|
errorDetails.HTTPStatus = resp.StatusCode
|
|
|
|
}
|
|
|
|
|
2018-12-06 21:50:17 +00:00
|
|
|
// Check for errors we handle specifically
|
|
|
|
if errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr {
|
|
|
|
return &acme.NonceError{ProblemDetails: errorDetails}
|
|
|
|
}
|
|
|
|
|
|
|
|
return errorDetails
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|