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