From 71624f607a62749ecb38b23af430de8749571862 Mon Sep 17 00:00:00 2001 From: Tommie Gannert Date: Sat, 5 Dec 2015 21:32:53 +0000 Subject: [PATCH] Replace exponential back-off in validate with Retry-After header. Last paragraph of ACME spec, section 6.5: To check on the status of an authorization, the client sends a GET request to the authorization URI, and the server responds with the current authorization object. In responding to poll requests while the validation is still in progress, the server MUST return a 202 (Accepted) response with a Retry-After header field. --- acme/client.go | 33 ++++++++++++++------------------- acme/client_test.go | 4 +--- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/acme/client.go b/acme/client.go index 777cc8e0..025ae915 100644 --- a/acme/client.go +++ b/acme/client.go @@ -77,7 +77,7 @@ func NewClient(caDirURL string, user User, keyBits int, optSolvers []string) (*C } var dir directory - if err := getJSON(caDirURL, &dir); err != nil { + if _, err := getJSON(caDirURL, &dir); err != nil { return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err) } @@ -634,23 +634,16 @@ func parseLinks(links []string) map[string]string { return linkMap } -var ( - pollInterval = 1 * time.Second - maxPollInterval = 15 * time.Minute -) - // validate makes the ACME server start validating a // challenge response, only returning once it is done. func validate(j *jws, uri string, chlng challenge) error { var challengeResponse challenge - _, err := postJSON(j, uri, chlng, &challengeResponse) + hdr, err := postJSON(j, uri, chlng, &challengeResponse) if err != nil { return err } - delay := pollInterval - // After the path is sent, the ACME server will access our server. // Repeatedly check the server for an updated status on our request. for { @@ -666,14 +659,16 @@ func validate(j *jws, uri string, chlng challenge) error { return errors.New("The server returned an unexpected state.") } - // Poll with exponential back-off. - time.Sleep(delay) - delay *= 2 - if delay > maxPollInterval { - delay = maxPollInterval + ra, err := strconv.Atoi(hdr.Get("Retry-After")) + if err != nil { + // The ACME server MUST return a Retry-After. + // If it doesn't, we'll just poll hard. + ra = 1 } + time.Sleep(time.Duration(ra) * time.Second) - if err := getJSON(uri, &challengeResponse); err != nil { + hdr, err = getJSON(uri, &challengeResponse) + if err != nil { return err } } @@ -683,18 +678,18 @@ func validate(j *jws, uri string, chlng challenge) error { // getJSON performs an HTTP GET request and parses the response body // as JSON, into the provided respBody object. -func getJSON(uri string, respBody interface{}) error { +func getJSON(uri string, respBody interface{}) (http.Header, error) { resp, err := http.Get(uri) if err != nil { - return fmt.Errorf("failed to get %q: %v", uri, err) + return nil, fmt.Errorf("failed to get %q: %v", uri, err) } defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { - return handleHTTPError(resp) + return resp.Header, handleHTTPError(resp) } - return json.NewDecoder(resp.Body).Decode(respBody) + return resp.Header, json.NewDecoder(resp.Body).Decode(respBody) } // postJSON performs an HTTP POST request and parses the response body diff --git a/acme/client_test.go b/acme/client_test.go index e62e3731..7a2f586a 100644 --- a/acme/client_test.go +++ b/acme/client_test.go @@ -84,13 +84,11 @@ func TestNewClientOptPort(t *testing.T) { } func TestValidate(t *testing.T) { - // Disable polling delay in validate for faster tests. - pollInterval = 0 - var statuses []string ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Minimal stub ACME server for validation. w.Header().Add("Replay-Nonce", "12345") + w.Header().Add("Retry-After", "0") switch r.Method { case "HEAD": case "POST":