diff --git a/acme/client.go b/acme/client.go index 6897ed4b..598f5692 100644 --- a/acme/client.go +++ b/acme/client.go @@ -670,3 +670,82 @@ func parseLinks(links []string) map[string]string { return linkMap } + +// 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 + + if err := postJSON(j, uri, chlng, &challengeResponse); err != nil { + return err + } + + interval := 1 * time.Second + maxInterval := 15 * time.Minute + + // After the path is sent, the ACME server will access our server. + // Repeatedly check the server for an updated status on our request. + for { + switch challengeResponse.Status { + case "valid": + logf("The server validated our request") + return nil + case "pending": + break + case "invalid": + return errors.New("The server could not validate our request.") + default: + return errors.New("The server returned an unexpected state.") + } + + // Poll with exponential back-off. + time.Sleep(interval) + interval *= 2 + if interval > maxInterval { + interval = maxInterval + } + + if err := getJSON(uri, &challengeResponse); err != nil { + return err + } + } + + return nil +} + +// 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 { + resp, err := http.Get(uri) + if err != nil { + return fmt.Errorf("failed to get %q: %v", uri, err) + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusBadRequest { + return handleHTTPError(resp) + } + + return json.NewDecoder(resp.Body).Decode(respBody) +} + +// postJSON performs an HTTP POST request and parses the response body +// as JSON, into the provided respBody object. +func postJSON(j *jws, uri string, reqBody, respBody interface{}) error { + jsonBytes, err := json.Marshal(reqBody) + if err != nil { + return errors.New("Failed to marshal network message...") + } + + resp, err := j.post(uri, jsonBytes) + if err != nil { + return fmt.Errorf("Failed to post JWS message. -> %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusBadRequest { + return handleHTTPError(resp) + } + + return json.NewDecoder(resp.Body).Decode(respBody) +} diff --git a/acme/http_challenge.go b/acme/http_challenge.go index 8ace6795..979ec4b3 100644 --- a/acme/http_challenge.go +++ b/acme/http_challenge.go @@ -1,13 +1,10 @@ package acme import ( - "encoding/json" - "errors" "fmt" "net" "net/http" "strings" - "time" ) type httpChallenge struct { @@ -47,49 +44,7 @@ func (s *httpChallenge) Solve(chlng challenge, domain string) error { close(s.end) }() - jsonBytes, err := json.Marshal(challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) - if err != nil { - return errors.New("Failed to marshal network message...") - } - - // Tell the server we handle HTTP-01 - resp, err := s.jws.post(chlng.URI, jsonBytes) - if err != nil { - return fmt.Errorf("Failed to post JWS message. -> %v", err) - } - - // After the path is sent, the ACME server will access our server. - // Repeatedly check the server for an updated status on our request. - var challengeResponse challenge -Loop: - for { - if resp.StatusCode >= http.StatusBadRequest { - return handleHTTPError(resp) - } - - err = json.NewDecoder(resp.Body).Decode(&challengeResponse) - resp.Body.Close() - if err != nil { - return err - } - - switch challengeResponse.Status { - case "valid": - logf("The server validated our request") - break Loop - case "pending": - break - case "invalid": - return errors.New("The server could not validate our request.") - default: - return errors.New("The server returned an unexpected state.") - } - - time.Sleep(1 * time.Second) - resp, err = http.Get(chlng.URI) - } - - return nil + return validate(s.jws, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) } func (s *httpChallenge) startHTTPServer(domain string, token string, keyAuth string) { diff --git a/acme/tls_sni_challenge.go b/acme/tls_sni_challenge.go index d5f249bd..634bf899 100644 --- a/acme/tls_sni_challenge.go +++ b/acme/tls_sni_challenge.go @@ -5,12 +5,9 @@ import ( "crypto/sha256" "crypto/tls" "encoding/hex" - "encoding/json" - "errors" "fmt" "net" "net/http" - "time" ) type tlsSNIChallenge struct { @@ -57,49 +54,7 @@ func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error { close(t.end) }() - jsonBytes, err := json.Marshal(challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) - if err != nil { - return errors.New("Failed to marshal network message...") - } - - // Tell the server we handle TLS-SNI-01 - resp, err := t.jws.post(chlng.URI, jsonBytes) - if err != nil { - return fmt.Errorf("Failed to post JWS message. -> %v", err) - } - - // After the path is sent, the ACME server will access our server. - // Repeatedly check the server for an updated status on our request. - var challengeResponse challenge -Loop: - for { - if resp.StatusCode >= http.StatusBadRequest { - return handleHTTPError(resp) - } - - err = json.NewDecoder(resp.Body).Decode(&challengeResponse) - resp.Body.Close() - if err != nil { - return err - } - - switch challengeResponse.Status { - case "valid": - logf("The server validated our request") - break Loop - case "pending": - break - case "invalid": - return errors.New("The server could not validate our request.") - default: - return errors.New("The server returned an unexpected state.") - } - - time.Sleep(1 * time.Second) - resp, err = http.Get(chlng.URI) - } - - return nil + return validate(t.jws, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) } func (t *tlsSNIChallenge) generateCertificate(keyAuth string) (tls.Certificate, error) {