Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 79 additions & 53 deletions acme/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"io/ioutil"
"log"
"net"
"net/http"
"regexp"
"strconv"
"strings"
Expand All @@ -22,6 +23,9 @@ var (
Logger *log.Logger
)

// maxBodySize is the maximum size of body that we will read.
const maxBodySize = 1024 * 1024

// logf writes a log entry. It uses Logger if not
// nil, otherwise it uses the default log.Logger.
func logf(format string, args ...interface{}) {
Expand Down Expand Up @@ -610,73 +614,95 @@ func (c *Client) requestCertificateForCsr(authz []authorizationResource, bundle
return CertificateResource{}, err
}

cerRes := CertificateResource{
certRes := CertificateResource{
Domain: commonName.Domain,
CertURL: resp.Header.Get("Location"),
PrivateKey: privateKeyPem}

for {
switch resp.StatusCode {
case 201, 202:
cert, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
resp.Body.Close()
if err != nil {
return CertificateResource{}, err
}
PrivateKey: privateKeyPem,
}

// The server returns a body with a length of zero if the
// certificate was not ready at the time this request completed.
// Otherwise the body is the certificate.
if len(cert) > 0 {
maxChecks := 1000
for i := 0; i < maxChecks; i++ {
done, err := c.checkCertResponse(resp, &certRes, bundle)
resp.Body.Close()
if err != nil {
return CertificateResource{}, err
}
if done {
break
}
if i == maxChecks-1 {
return CertificateResource{}, fmt.Errorf("polled for certificate %d times; giving up", i)
}
resp, err = httpGet(certRes.CertURL)
if err != nil {
return CertificateResource{}, err
}
}

cerRes.CertStableURL = resp.Header.Get("Content-Location")
cerRes.AccountRef = c.user.GetRegistration().URI
return certRes, nil
}

issuedCert := pemEncode(derCertificateBytes(cert))
// checkCertResponse checks resp to see if a certificate is contained in the
// response, and if so, loads it into certRes and returns true. If the cert
// is not yet ready, it returns false. This function honors the waiting period
// required by the Retry-After header of the response, if specified. This
// function may read from resp.Body but does NOT close it. The certRes input
// should already have the Domain (common name) field populated. If bundle is
// true, the certificate will be bundled with the issuer's cert.
func (c *Client) checkCertResponse(resp *http.Response, certRes *CertificateResource, bundle bool) (bool, error) {
switch resp.StatusCode {
case 201, 202:
cert, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
if err != nil {
return false, err
}

// The issuer certificate link is always supplied via an "up" link
// in the response headers of a new certificate.
links := parseLinks(resp.Header["Link"])
issuerCert, err := c.getIssuerCertificate(links["up"])
if err != nil {
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", commonName.Domain, err)
} else {
issuerCert = pemEncode(derCertificateBytes(issuerCert))

// If bundle is true, we want to return a certificate bundle.
// To do this, we append the issuer cert to the issued cert.
if bundle {
issuedCert = append(issuedCert, issuerCert...)
}
}
// The server returns a body with a length of zero if the
// certificate was not ready at the time this request completed.
// Otherwise the body is the certificate.
if len(cert) > 0 {
certRes.CertStableURL = resp.Header.Get("Content-Location")
certRes.AccountRef = c.user.GetRegistration().URI

cerRes.Certificate = issuedCert
cerRes.IssuerCertificate = issuerCert
logf("[INFO][%s] Server responded with a certificate.", commonName.Domain)
return cerRes, nil
}
issuedCert := pemEncode(derCertificateBytes(cert))

// The certificate was granted but is not yet issued.
// Check retry-after and loop.
ra := resp.Header.Get("Retry-After")
retryAfter, err := strconv.Atoi(ra)
// The issuer certificate link is always supplied via an "up" link
// in the response headers of a new certificate.
links := parseLinks(resp.Header["Link"])
issuerCert, err := c.getIssuerCertificate(links["up"])
if err != nil {
return CertificateResource{}, err
}
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err)
} else {
issuerCert = pemEncode(derCertificateBytes(issuerCert))

logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", commonName.Domain, retryAfter)
time.Sleep(time.Duration(retryAfter) * time.Second)
// If bundle is true, we want to return a certificate bundle.
// To do this, we append the issuer cert to the issued cert.
if bundle {
issuedCert = append(issuedCert, issuerCert...)
}
}

break
default:
return CertificateResource{}, handleHTTPError(resp)
certRes.Certificate = issuedCert
certRes.IssuerCertificate = issuerCert
logf("[INFO][%s] Server responded with a certificate.", certRes.Domain)
return true, nil
}

resp, err = httpGet(cerRes.CertURL)
// The certificate was granted but is not yet issued.
// Check retry-after and loop.
ra := resp.Header.Get("Retry-After")
retryAfter, err := strconv.Atoi(ra)
if err != nil {
return CertificateResource{}, err
return false, err
}

logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", certRes.Domain, retryAfter)
time.Sleep(time.Duration(retryAfter) * time.Second)

return false, nil
default:
return false, handleHTTPError(resp)
}
}

Expand All @@ -689,7 +715,7 @@ func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
}
defer resp.Body.Close()

issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
if err != nil {
return nil, err
}
Expand Down
15 changes: 5 additions & 10 deletions acme/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import (
"strings"
)

const (
tosAgreementError = "Must agree to subscriber agreement before any further actions"
)
const tosAgreementError = "Must agree to subscriber agreement before any further actions"

// RemoteError is the base type for all errors specific to the ACME protocol.
type RemoteError struct {
Expand Down Expand Up @@ -54,20 +52,17 @@ func (c challengeError) Error() string {
func handleHTTPError(resp *http.Response) error {
var errorDetail RemoteError

contenType := resp.Header.Get("Content-Type")
// try to decode the content as JSON
if contenType == "application/json" || contenType == "application/problem+json" {
decoder := json.NewDecoder(resp.Body)
err := decoder.Decode(&errorDetail)
contentType := resp.Header.Get("Content-Type")
if contentType == "application/json" || contentType == "application/problem+json" {
err := json.NewDecoder(resp.Body).Decode(&errorDetail)
if err != nil {
return err
}
} else {
detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
if err != nil {
return err
}

errorDetail.Detail = string(detailBytes)
}

Expand Down
4 changes: 3 additions & 1 deletion acme/jws.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ func keyAsJWK(key interface{}) *jose.JsonWebKey {
}
}

// Posts a JWS signed message to the specified URL
// Posts a JWS signed message to the specified URL.
// It does NOT close the response body, so the caller must
// do that if no error was returned.
func (j *jws) post(url string, content []byte) (*http.Response, error) {
signedContent, err := j.signContent(content)
if err != nil {
Expand Down