diff --git a/certificate/renewal.go b/certificate/renewal.go index 4f420301..66c93acb 100644 --- a/certificate/renewal.go +++ b/certificate/renewal.go @@ -21,6 +21,12 @@ type RenewalInfoRequest struct { // RenewalInfoResponse is a wrapper around acme.RenewalInfoResponse that provides a method for determining when to renew a certificate. type RenewalInfoResponse struct { acme.RenewalInfoResponse + + // RetryAfter header indicating the polling interval that the ACME server recommends. + // Conforming clients SHOULD query the renewalInfo URL again after the RetryAfter period has passed, + // as the server may provide a different suggestedWindow. + // https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-4.2 + RetryAfter time.Duration } // ShouldRenewAt determines the optimal renewal time based on the current time (UTC),renewal window suggest by ARI, and the client's willingness to sleep. @@ -81,6 +87,14 @@ func (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse if err != nil { return nil, err } + + if retry := resp.Header.Get("Retry-After"); retry != "" { + info.RetryAfter, err = time.ParseDuration(retry + "s") + if err != nil { + return nil, err + } + } + return &info, nil } diff --git a/certificate/renewal_test.go b/certificate/renewal_test.go index e883a40c..9f20e374 100644 --- a/certificate/renewal_test.go +++ b/certificate/renewal_test.go @@ -50,6 +50,7 @@ func TestCertifier_GetRenewalInfo(t *testing.T) { } w.Header().Set("Content-Type", "application/json") + w.Header().Set("Retry-After", "21600") w.WriteHeader(http.StatusOK) _, wErr := w.Write([]byte(`{ "suggestedWindow": { @@ -76,6 +77,7 @@ func TestCertifier_GetRenewalInfo(t *testing.T) { assert.Equal(t, "2020-03-17T17:51:09Z", ri.SuggestedWindow.Start.Format(time.RFC3339)) assert.Equal(t, "2020-03-17T18:21:09Z", ri.SuggestedWindow.End.Format(time.RFC3339)) assert.Equal(t, "https://aricapable.ca/docs/renewal-advice/", ri.ExplanationURL) + assert.Equal(t, time.Duration(21600000000000), ri.RetryAfter) } func TestCertifier_GetRenewalInfo_errors(t *testing.T) { @@ -135,13 +137,14 @@ func TestRenewalInfoResponse_ShouldRenew(t *testing.T) { t.Run("Window is in the past", func(t *testing.T) { ri := RenewalInfoResponse{ - acme.RenewalInfoResponse{ + RenewalInfoResponse: acme.RenewalInfoResponse{ SuggestedWindow: acme.Window{ Start: now.Add(-2 * time.Hour), End: now.Add(-1 * time.Hour), }, ExplanationURL: "", }, + RetryAfter: 0, } rt := ri.ShouldRenewAt(now, 0) @@ -151,13 +154,14 @@ func TestRenewalInfoResponse_ShouldRenew(t *testing.T) { t.Run("Window is in the future", func(t *testing.T) { ri := RenewalInfoResponse{ - acme.RenewalInfoResponse{ + RenewalInfoResponse: acme.RenewalInfoResponse{ SuggestedWindow: acme.Window{ Start: now.Add(1 * time.Hour), End: now.Add(2 * time.Hour), }, ExplanationURL: "", }, + RetryAfter: 0, } rt := ri.ShouldRenewAt(now, 0) @@ -166,13 +170,14 @@ func TestRenewalInfoResponse_ShouldRenew(t *testing.T) { t.Run("Window is in the future, but caller is willing to sleep", func(t *testing.T) { ri := RenewalInfoResponse{ - acme.RenewalInfoResponse{ + RenewalInfoResponse: acme.RenewalInfoResponse{ SuggestedWindow: acme.Window{ Start: now.Add(1 * time.Hour), End: now.Add(2 * time.Hour), }, ExplanationURL: "", }, + RetryAfter: 0, } rt := ri.ShouldRenewAt(now, 2*time.Hour) @@ -182,13 +187,14 @@ func TestRenewalInfoResponse_ShouldRenew(t *testing.T) { t.Run("Window is in the future, but caller isn't willing to sleep long enough", func(t *testing.T) { ri := RenewalInfoResponse{ - acme.RenewalInfoResponse{ + RenewalInfoResponse: acme.RenewalInfoResponse{ SuggestedWindow: acme.Window{ Start: now.Add(1 * time.Hour), End: now.Add(2 * time.Hour), }, ExplanationURL: "", }, + RetryAfter: 0, } rt := ri.ShouldRenewAt(now, 59*time.Minute)