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:
parent
ce8fb060cb
commit
f5d538caab
3 changed files with 96 additions and 73 deletions
150
acme/client.go
150
acme/client.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue