diff --git a/acme/acme.go b/acme/acme.go index 7a51284f91..19d9ac465b 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -381,7 +381,7 @@ func (c *Client) authorize(ctx context.Context, typ, val string) (*Authorization if v.Status != StatusPending && v.Status != StatusValid { return nil, fmt.Errorf("acme: unexpected status: %s", v.Status) } - return v.authorization(res.Header.Get("Location")), nil + return v.authorization(res.Header.Get("Location"), 0), nil } // GetAuthorization retrieves an authorization identified by the given URL. @@ -402,7 +402,8 @@ func (c *Client) GetAuthorization(ctx context.Context, url string) (*Authorizati if err := json.NewDecoder(res.Body).Decode(&v); err != nil { return nil, fmt.Errorf("acme: invalid response: %v", err) } - return v.authorization(url), nil + d := retryAfter(res.Header.Get("Retry-After")) + return v.authorization(url, d), nil } // RevokeAuthorization relinquishes an existing authorization identified @@ -460,7 +461,7 @@ func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorizat case err != nil: // Skip and retry. case raw.Status == StatusValid: - return raw.authorization(url), nil + return raw.authorization(url, 0), nil case raw.Status == StatusInvalid: return nil, raw.error(url) } @@ -505,7 +506,8 @@ func (c *Client) GetChallenge(ctx context.Context, url string) (*Challenge, erro if err := json.NewDecoder(res.Body).Decode(&v); err != nil { return nil, fmt.Errorf("acme: invalid response: %v", err) } - return v.challenge(), nil + d := retryAfter(res.Header.Get("Retry-After")) + return v.challenge(d), nil } // Accept informs the server that the client accepts one of its challenges @@ -534,7 +536,7 @@ func (c *Client) Accept(ctx context.Context, chal *Challenge) (*Challenge, error if err := json.NewDecoder(res.Body).Decode(&v); err != nil { return nil, fmt.Errorf("acme: invalid response: %v", err) } - return v.challenge(), nil + return v.challenge(0), nil } // DNS01ChallengeRecord returns a DNS record value for a dns-01 challenge response. diff --git a/acme/rfc8555.go b/acme/rfc8555.go index 3152e531b6..42e76c44ee 100644 --- a/acme/rfc8555.go +++ b/acme/rfc8555.go @@ -318,6 +318,7 @@ func responseOrder(res *http.Response) (*Order, error) { AuthzURLs: v.Authorizations, FinalizeURL: v.Finalize, CertURL: v.Certificate, + RetryAfter: retryAfter(res.Header.Get("Retry-After")), } for _, id := range v.Identifiers { o.Identifiers = append(o.Identifiers, AuthzID{Type: id.Type, Value: id.Value}) diff --git a/acme/types.go b/acme/types.go index c466645ca1..da7c6a579c 100644 --- a/acme/types.go +++ b/acme/types.go @@ -366,6 +366,13 @@ type Order struct { // The error that occurred while processing the order as received from a CA, if any. Error *Error + + // RetryAfter specifies how long the client should wait before polling the order again, + // based on the Retry-After header provided by the server while the order is in the + // StatusProcessing state. + // + // See RFC 8555 Section 7.4. + RetryAfter time.Duration } // OrderOption allows customizing Client.AuthorizeOrder call. @@ -426,6 +433,14 @@ type Authorization struct { // // This field is unused in RFC 8555. Combinations [][]int + + // RetryAfter specifies how long the client should wait before polling the + // authorization resource again, if indicated by the server. + // This corresponds to the optional Retry-After HTTP header included in a + // 200 (OK) response when the authorization is still StatusPending. + // + // See RFC 8555 Section 7.5.1. + RetryAfter time.Duration } // AuthzID is an identifier that an account is authorized to represent. @@ -471,7 +486,7 @@ type wireAuthz struct { Error *wireError } -func (z *wireAuthz) authorization(uri string) *Authorization { +func (z *wireAuthz) authorization(uri string, retryAfter time.Duration) *Authorization { a := &Authorization{ URI: uri, Status: z.Status, @@ -480,9 +495,10 @@ func (z *wireAuthz) authorization(uri string) *Authorization { Wildcard: z.Wildcard, Challenges: make([]*Challenge, len(z.Challenges)), Combinations: z.Combinations, // shallow copy + RetryAfter: retryAfter, } for i, v := range z.Challenges { - a.Challenges[i] = v.challenge() + a.Challenges[i] = v.challenge(0) } return a } @@ -542,6 +558,13 @@ type Challenge struct { // where the client must send additional data for the server to validate // the challenge. Payload json.RawMessage + + // RetryAfter specifies how long the client should wait before polling the + // challenge again, based on the Retry-After header provided by the server + // while the challenge is in the StatusProcessing state. + // + // See RFC 8555 Section 8.2. + RetryAfter time.Duration } // wireChallenge is ACME JSON challenge representation. @@ -555,12 +578,13 @@ type wireChallenge struct { Error *wireError } -func (c *wireChallenge) challenge() *Challenge { +func (c *wireChallenge) challenge(retryAfter time.Duration) *Challenge { v := &Challenge{ - URI: c.URL, - Type: c.Type, - Token: c.Token, - Status: c.Status, + URI: c.URL, + Type: c.Type, + Token: c.Token, + Status: c.Status, + RetryAfter: retryAfter, } if v.URI == "" { v.URI = c.URI // c.URL was empty; use legacy