api.go raw
1 package api
2
3 import (
4 "bytes"
5 "context"
6 "crypto"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "net/http"
11 "time"
12
13 "github.com/cenkalti/backoff/v5"
14 "github.com/go-acme/lego/v4/acme"
15 "github.com/go-acme/lego/v4/acme/api/internal/nonces"
16 "github.com/go-acme/lego/v4/acme/api/internal/secure"
17 "github.com/go-acme/lego/v4/acme/api/internal/sender"
18 "github.com/go-acme/lego/v4/log"
19 )
20
21 // Core ACME/LE core API.
22 type Core struct {
23 doer *sender.Doer
24 nonceManager *nonces.Manager
25 jws *secure.JWS
26 directory acme.Directory
27 HTTPClient *http.Client
28
29 common service // Reuse a single struct instead of allocating one for each service on the heap.
30 Accounts *AccountService
31 Authorizations *AuthorizationService
32 Certificates *CertificateService
33 Challenges *ChallengeService
34 Orders *OrderService
35 }
36
37 // New Creates a new Core.
38 func New(httpClient *http.Client, userAgent, caDirURL, kid string, privateKey crypto.PrivateKey) (*Core, error) {
39 doer := sender.NewDoer(httpClient, userAgent)
40
41 dir, err := getDirectory(doer, caDirURL)
42 if err != nil {
43 return nil, err
44 }
45
46 nonceManager := nonces.NewManager(doer, dir.NewNonceURL)
47
48 jws := secure.NewJWS(privateKey, kid, nonceManager)
49
50 c := &Core{doer: doer, nonceManager: nonceManager, jws: jws, directory: dir, HTTPClient: httpClient}
51
52 c.common.core = c
53 c.Accounts = (*AccountService)(&c.common)
54 c.Authorizations = (*AuthorizationService)(&c.common)
55 c.Certificates = (*CertificateService)(&c.common)
56 c.Challenges = (*ChallengeService)(&c.common)
57 c.Orders = (*OrderService)(&c.common)
58
59 return c, nil
60 }
61
62 // post performs an HTTP POST request and parses the response body as JSON,
63 // into the provided respBody object.
64 func (a *Core) post(uri string, reqBody, response any) (*http.Response, error) {
65 content, err := json.Marshal(reqBody)
66 if err != nil {
67 return nil, errors.New("failed to marshal message")
68 }
69
70 return a.retrievablePost(uri, content, response)
71 }
72
73 // postAsGet performs an HTTP POST ("POST-as-GET") request.
74 // https://www.rfc-editor.org/rfc/rfc8555.html#section-6.3
75 func (a *Core) postAsGet(uri string, response any) (*http.Response, error) {
76 return a.retrievablePost(uri, []byte{}, response)
77 }
78
79 func (a *Core) retrievablePost(uri string, content []byte, response any) (*http.Response, error) {
80 ctx := context.Background()
81
82 // during tests, allow to support ~90% of bad nonce with a minimum of attempts.
83 bo := backoff.NewExponentialBackOff()
84 bo.InitialInterval = 200 * time.Millisecond
85 bo.MaxInterval = 5 * time.Second
86
87 operation := func() (*http.Response, error) {
88 resp, err := a.signedPost(uri, content, response)
89 if err != nil {
90 // Retry if the nonce was invalidated
91 var e *acme.NonceError
92 if errors.As(err, &e) {
93 return resp, err
94 }
95
96 return resp, backoff.Permanent(err)
97 }
98
99 return resp, nil
100 }
101
102 notify := func(err error, duration time.Duration) {
103 log.Infof("retry due to: %v", err)
104 }
105
106 return backoff.Retry(ctx, operation,
107 backoff.WithBackOff(bo),
108 backoff.WithMaxElapsedTime(20*time.Second),
109 backoff.WithNotify(notify))
110 }
111
112 func (a *Core) signedPost(uri string, content []byte, response any) (*http.Response, error) {
113 signedContent, err := a.jws.SignContent(uri, content)
114 if err != nil {
115 return nil, fmt.Errorf("failed to post JWS message: failed to sign content: %w", err)
116 }
117
118 signedBody := bytes.NewBufferString(signedContent.FullSerialize())
119
120 resp, err := a.doer.Post(uri, signedBody, "application/jose+json", response)
121
122 // nonceErr is ignored to keep the root error.
123 nonce, nonceErr := nonces.GetFromResponse(resp)
124 if nonceErr == nil {
125 a.nonceManager.Push(nonce)
126 }
127
128 return resp, err
129 }
130
131 func (a *Core) signEABContent(newAccountURL, kid string, hmac []byte) ([]byte, error) {
132 eabJWS, err := a.jws.SignEABContent(newAccountURL, kid, hmac)
133 if err != nil {
134 return nil, err
135 }
136
137 return []byte(eabJWS.FullSerialize()), nil
138 }
139
140 // GetKeyAuthorization Gets the key authorization.
141 func (a *Core) GetKeyAuthorization(token string) (string, error) {
142 return a.jws.GetKeyAuthorization(token)
143 }
144
145 func (a *Core) GetDirectory() acme.Directory {
146 return a.directory
147 }
148
149 func getDirectory(do *sender.Doer, caDirURL string) (acme.Directory, error) {
150 var dir acme.Directory
151 if _, err := do.Get(caDirURL, &dir); err != nil {
152 return dir, fmt.Errorf("get directory at '%s': %w", caDirURL, err)
153 }
154
155 if dir.NewAccountURL == "" {
156 return dir, errors.New("directory missing new registration URL")
157 }
158
159 if dir.NewOrderURL == "" {
160 return dir, errors.New("directory missing new order URL")
161 }
162
163 return dir, nil
164 }
165