Close response body in error case and close first one (#341)

* Close response body in error case

* Ensure the body of both responses is closed when polling for cert

Also make a new const of maxBodySize, and cap the number of polls
to a maximum of 1000.

* More correct placement for polling limit

* Move const to the top
This commit is contained in:
Matt Holt 2017-01-15 08:54:49 -07:00 committed by xenolf
parent ce8fb060cb
commit f5d538caab
3 changed files with 96 additions and 73 deletions

View file

@ -11,6 +11,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
"net/http"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -22,6 +23,9 @@ var (
Logger *log.Logger 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 // logf writes a log entry. It uses Logger if not
// nil, otherwise it uses the default log.Logger. // nil, otherwise it uses the default log.Logger.
func logf(format string, args ...interface{}) { func logf(format string, args ...interface{}) {
@ -610,73 +614,95 @@ func (c *Client) requestCertificateForCsr(authz []authorizationResource, bundle
return CertificateResource{}, err return CertificateResource{}, err
} }
cerRes := CertificateResource{ certRes := CertificateResource{
Domain: commonName.Domain, Domain: commonName.Domain,
CertURL: resp.Header.Get("Location"), CertURL: resp.Header.Get("Location"),
PrivateKey: privateKeyPem} PrivateKey: privateKeyPem,
}
for { maxChecks := 1000
switch resp.StatusCode { for i := 0; i < maxChecks; i++ {
case 201, 202: done, err := c.checkCertResponse(resp, &certRes, bundle)
cert, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024)) resp.Body.Close()
resp.Body.Close()
if err != nil {
return CertificateResource{}, err
}
// 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 {
cerRes.CertStableURL = resp.Header.Get("Content-Location")
cerRes.AccountRef = c.user.GetRegistration().URI
issuedCert := pemEncode(derCertificateBytes(cert))
// 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...)
}
}
cerRes.Certificate = issuedCert
cerRes.IssuerCertificate = issuerCert
logf("[INFO][%s] Server responded with a certificate.", commonName.Domain)
return cerRes, nil
}
// 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
}
logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", commonName.Domain, retryAfter)
time.Sleep(time.Duration(retryAfter) * time.Second)
break
default:
return CertificateResource{}, handleHTTPError(resp)
}
resp, err = httpGet(cerRes.CertURL)
if err != nil { if err != nil {
return CertificateResource{}, err 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
}
}
return certRes, nil
}
// 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 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
issuedCert := pemEncode(derCertificateBytes(cert))
// 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", certRes.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...)
}
}
certRes.Certificate = issuedCert
certRes.IssuerCertificate = issuerCert
logf("[INFO][%s] Server responded with a certificate.", certRes.Domain)
return true, nil
}
// 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 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)
} }
} }
@ -689,7 +715,7 @@ func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024)) issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -8,9 +8,7 @@ import (
"strings" "strings"
) )
const ( const tosAgreementError = "Must agree to subscriber agreement before any further actions"
tosAgreementError = "Must agree to subscriber agreement before any further actions"
)
// RemoteError is the base type for all errors specific to the ACME protocol. // RemoteError is the base type for all errors specific to the ACME protocol.
type RemoteError struct { type RemoteError struct {
@ -54,20 +52,17 @@ func (c challengeError) Error() string {
func handleHTTPError(resp *http.Response) error { func handleHTTPError(resp *http.Response) error {
var errorDetail RemoteError var errorDetail RemoteError
contenType := resp.Header.Get("Content-Type") contentType := resp.Header.Get("Content-Type")
// try to decode the content as JSON if contentType == "application/json" || contentType == "application/problem+json" {
if contenType == "application/json" || contenType == "application/problem+json" { err := json.NewDecoder(resp.Body).Decode(&errorDetail)
decoder := json.NewDecoder(resp.Body)
err := decoder.Decode(&errorDetail)
if err != nil { if err != nil {
return err return err
} }
} else { } else {
detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024)) detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
if err != nil { if err != nil {
return err return err
} }
errorDetail.Detail = string(detailBytes) errorDetail.Detail = string(detailBytes)
} }

View file

@ -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) { func (j *jws) post(url string, content []byte) (*http.Response, error) {
signedContent, err := j.signContent(content) signedContent, err := j.signContent(content)
if err != nil { if err != nil {