forked from TrueCloudLab/lego
Add TOSError and change ObtainCertificates to return errors by domain.
This commit is contained in:
parent
ee58d205a5
commit
a2867a0c18
3 changed files with 80 additions and 41 deletions
|
@ -183,17 +183,30 @@ func (c *Client) AgreeToTOS() error {
|
|||
// PEM encoded byte slices.
|
||||
// If bundle is true, the []byte contains both the issuer certificate and
|
||||
// your issued certificate as a bundle.
|
||||
func (c *Client) ObtainCertificates(domains []string, bundle bool) ([]CertificateResource, error) {
|
||||
func (c *Client) ObtainCertificates(domains []string, bundle bool) ([]CertificateResource, map[string]error) {
|
||||
logger().Print("Obtaining certificates...")
|
||||
challenges := c.getChallenges(domains)
|
||||
challenges, failures := c.getChallenges(domains)
|
||||
if len(challenges) == 0 {
|
||||
return nil, failures
|
||||
}
|
||||
|
||||
err := c.solveChallenges(challenges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
for k, v := range err {
|
||||
failures[k] = v
|
||||
}
|
||||
|
||||
if len(failures) == len(domains) {
|
||||
return nil, failures
|
||||
}
|
||||
|
||||
logger().Print("Validations succeeded. Getting certificates")
|
||||
|
||||
return c.requestCertificates(challenges, bundle)
|
||||
certs, err := c.requestCertificates(challenges, bundle)
|
||||
for k, v := range err {
|
||||
failures[k] = v
|
||||
}
|
||||
|
||||
return certs, failures
|
||||
}
|
||||
|
||||
// RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
|
||||
|
@ -299,9 +312,9 @@ func (c *Client) RenewCertificate(cert CertificateResource, revokeOld bool, bund
|
|||
return cert, nil
|
||||
}
|
||||
|
||||
newCerts, err := c.ObtainCertificates([]string{cert.Domain}, bundle)
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
newCerts, failures := c.ObtainCertificates([]string{cert.Domain}, bundle)
|
||||
if len(failures) > 0 {
|
||||
return CertificateResource{}, failures[cert.Domain]
|
||||
}
|
||||
|
||||
if revokeOld {
|
||||
|
@ -313,8 +326,9 @@ func (c *Client) RenewCertificate(cert CertificateResource, revokeOld bool, bund
|
|||
|
||||
// Looks through the challenge combinations to find a solvable match.
|
||||
// Then solves the challenges in series and returns.
|
||||
func (c *Client) solveChallenges(challenges []*authorizationResource) error {
|
||||
func (c *Client) solveChallenges(challenges []*authorizationResource) map[string]error {
|
||||
// loop through the resources, basically through the domains.
|
||||
failures := make(map[string]error)
|
||||
for _, authz := range challenges {
|
||||
// no solvers - no solving
|
||||
if solvers := c.chooseSolvers(authz.Body, authz.Domain); solvers != nil {
|
||||
|
@ -322,15 +336,15 @@ func (c *Client) solveChallenges(challenges []*authorizationResource) error {
|
|||
// TODO: do not immediately fail if one domain fails to validate.
|
||||
err := solver.Solve(authz.Body.Challenges[i], authz.Domain)
|
||||
if err != nil {
|
||||
return err
|
||||
failures[authz.Domain] = err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("Could not determine solvers for %s", authz.Domain)
|
||||
failures[authz.Domain] = fmt.Errorf("Could not determine solvers for %s", authz.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return failures
|
||||
}
|
||||
|
||||
// Checks all combinations from the server and returns an array of
|
||||
|
@ -355,25 +369,25 @@ func (c *Client) chooseSolvers(auth authorization, domain string) map[int]solver
|
|||
}
|
||||
|
||||
// Get the challenges needed to proof our identifier to the ACME server.
|
||||
func (c *Client) getChallenges(domains []string) []*authorizationResource {
|
||||
resc, errc := make(chan *authorizationResource), make(chan error)
|
||||
func (c *Client) getChallenges(domains []string) ([]*authorizationResource, map[string]error) {
|
||||
resc, errc := make(chan *authorizationResource), make(chan domainError)
|
||||
|
||||
for _, domain := range domains {
|
||||
go func(domain string) {
|
||||
jsonBytes, err := json.Marshal(authorization{Resource: "new-authz", Identifier: identifier{Type: "dns", Value: domain}})
|
||||
if err != nil {
|
||||
errc <- err
|
||||
errc <- domainError{Domain: domain, Error: err}
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := c.jws.post(c.user.GetRegistration().NewAuthzURL, jsonBytes)
|
||||
if err != nil {
|
||||
errc <- err
|
||||
errc <- domainError{Domain: domain, Error: err}
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
errc <- handleHTTPError(resp)
|
||||
errc <- domainError{Domain: domain, Error: handleHTTPError(resp)}
|
||||
}
|
||||
|
||||
links := parseLinks(resp.Header["Link"])
|
||||
|
@ -386,7 +400,7 @@ func (c *Client) getChallenges(domains []string) []*authorizationResource {
|
|||
decoder := json.NewDecoder(resp.Body)
|
||||
err = decoder.Decode(&authz)
|
||||
if err != nil {
|
||||
errc <- err
|
||||
errc <- domainError{Domain: domain, Error: err}
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
|
@ -395,37 +409,39 @@ func (c *Client) getChallenges(domains []string) []*authorizationResource {
|
|||
}
|
||||
|
||||
var responses []*authorizationResource
|
||||
failures := make(map[string]error)
|
||||
for i := 0; i < len(domains); i++ {
|
||||
select {
|
||||
case res := <-resc:
|
||||
responses = append(responses, res)
|
||||
case err := <-errc:
|
||||
logger().Printf("%v", err)
|
||||
failures[err.Domain] = err.Error
|
||||
}
|
||||
}
|
||||
|
||||
close(resc)
|
||||
close(errc)
|
||||
|
||||
return responses
|
||||
return responses, failures
|
||||
}
|
||||
|
||||
// requestCertificates iterates all granted authorizations, creates RSA private keys and CSRs.
|
||||
// It then uses these to request a certificate from the CA and returns the list of successfully
|
||||
// granted certificates.
|
||||
func (c *Client) requestCertificates(challenges []*authorizationResource, bundle bool) ([]CertificateResource, error) {
|
||||
resc, errc := make(chan CertificateResource), make(chan error)
|
||||
func (c *Client) requestCertificates(challenges []*authorizationResource, bundle bool) ([]CertificateResource, map[string]error) {
|
||||
resc, errc := make(chan CertificateResource), make(chan domainError)
|
||||
for _, authz := range challenges {
|
||||
go c.requestCertificate(authz, resc, errc, bundle)
|
||||
}
|
||||
|
||||
var certs []CertificateResource
|
||||
failures := make(map[string]error)
|
||||
for i := 0; i < len(challenges); i++ {
|
||||
select {
|
||||
case res := <-resc:
|
||||
certs = append(certs, res)
|
||||
case err := <-errc:
|
||||
logger().Printf("%v", err)
|
||||
failures[err.Domain] = err.Error
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -435,30 +451,30 @@ func (c *Client) requestCertificates(challenges []*authorizationResource, bundle
|
|||
return certs, nil
|
||||
}
|
||||
|
||||
func (c *Client) requestCertificate(authz *authorizationResource, result chan CertificateResource, errc chan error, bundle bool) {
|
||||
func (c *Client) requestCertificate(authz *authorizationResource, result chan CertificateResource, errc chan domainError, bundle bool) {
|
||||
privKey, err := generatePrivateKey(rsakey, c.keyBits)
|
||||
if err != nil {
|
||||
errc <- err
|
||||
errc <- domainError{Domain: authz.Domain, Error: err}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: should the CSR be customizable?
|
||||
csr, err := generateCsr(privKey.(*rsa.PrivateKey), authz.Domain)
|
||||
if err != nil {
|
||||
errc <- err
|
||||
errc <- domainError{Domain: authz.Domain, Error: err}
|
||||
return
|
||||
}
|
||||
|
||||
csrString := base64.URLEncoding.EncodeToString(csr)
|
||||
jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: []string{authz.AuthURL}})
|
||||
if err != nil {
|
||||
errc <- err
|
||||
errc <- domainError{Domain: authz.Domain, Error: err}
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := c.jws.post(authz.NewCertURL, jsonBytes)
|
||||
if err != nil {
|
||||
errc <- err
|
||||
errc <- domainError{Domain: authz.Domain, Error: err}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -477,7 +493,7 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce
|
|||
cert, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
errc <- err
|
||||
errc <- domainError{Domain: authz.Domain, Error: err}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -517,7 +533,7 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce
|
|||
ra := resp.Header.Get("Retry-After")
|
||||
retryAfter, err := strconv.Atoi(ra)
|
||||
if err != nil {
|
||||
errc <- err
|
||||
errc <- domainError{Domain: authz.Domain, Error: err}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -526,13 +542,13 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce
|
|||
|
||||
break
|
||||
default:
|
||||
errc <- handleHTTPError(resp)
|
||||
errc <- domainError{Domain: authz.Domain, Error: handleHTTPError(resp)}
|
||||
return
|
||||
}
|
||||
|
||||
resp, err = http.Get(cerRes.CertURL)
|
||||
if err != nil {
|
||||
errc <- err
|
||||
errc <- domainError{Domain: authz.Domain, Error: err}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,19 +6,30 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
// Error is the base type for all errors specific to the ACME protocol.
|
||||
type Error struct {
|
||||
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 {
|
||||
StatusCode int `json:"status,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
func (e RemoteError) Error() string {
|
||||
return fmt.Sprintf("[%d] Type: %s Detail: %s", e.StatusCode, e.Type, e.Detail)
|
||||
}
|
||||
|
||||
// TOSError represents the error which is returned if the user needs to
|
||||
// accept the TOS.
|
||||
// TODO: include the new TOS url if we can somehow obtain it.
|
||||
type TOSError struct {
|
||||
RemoteError
|
||||
}
|
||||
|
||||
func handleHTTPError(resp *http.Response) error {
|
||||
var errorDetail Error
|
||||
var errorDetail RemoteError
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
err := decoder.Decode(&errorDetail)
|
||||
if err != nil {
|
||||
|
@ -26,5 +37,16 @@ func handleHTTPError(resp *http.Response) error {
|
|||
}
|
||||
|
||||
errorDetail.StatusCode = resp.StatusCode
|
||||
|
||||
// Check for errors we handle specifically
|
||||
if errorDetail.StatusCode == http.StatusForbidden && errorDetail.Detail == tosAgreementError {
|
||||
return TOSError{errorDetail}
|
||||
}
|
||||
|
||||
return errorDetail
|
||||
}
|
||||
|
||||
type domainError struct {
|
||||
Domain string
|
||||
Error error
|
||||
}
|
||||
|
|
|
@ -70,7 +70,6 @@ func saveCertRes(certRes acme.CertificateResource, conf *Configuration) {
|
|||
}
|
||||
|
||||
func run(c *cli.Context) {
|
||||
|
||||
conf, acc, client := setup(c)
|
||||
if acc.Registration == nil {
|
||||
reg, err := client.Register()
|
||||
|
@ -126,12 +125,14 @@ func run(c *cli.Context) {
|
|||
logger().Fatal("Please specify --domains")
|
||||
}
|
||||
|
||||
certs, err := client.ObtainCertificates(c.GlobalStringSlice("domains"), true)
|
||||
if err != nil {
|
||||
logger().Fatalf("Could not obtain certificates\n\t%v", err)
|
||||
certs, failures := client.ObtainCertificates(c.GlobalStringSlice("domains"), true)
|
||||
if len(failures) > 0 {
|
||||
for k, v := range failures {
|
||||
logger().Fatalf("[%s] Could not obtain certificates\n\t%v", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
err = checkFolder(conf.CertPath())
|
||||
err := checkFolder(conf.CertPath())
|
||||
if err != nil {
|
||||
logger().Fatalf("Cound not check/create path: %v", err)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue