solver_manager.go raw

   1  package resolver
   2  
   3  import (
   4  	"context"
   5  	"errors"
   6  	"fmt"
   7  	"sort"
   8  	"strconv"
   9  	"time"
  10  
  11  	"github.com/cenkalti/backoff/v5"
  12  	"github.com/go-acme/lego/v4/acme"
  13  	"github.com/go-acme/lego/v4/acme/api"
  14  	"github.com/go-acme/lego/v4/challenge"
  15  	"github.com/go-acme/lego/v4/challenge/dns01"
  16  	"github.com/go-acme/lego/v4/challenge/http01"
  17  	"github.com/go-acme/lego/v4/challenge/tlsalpn01"
  18  	"github.com/go-acme/lego/v4/log"
  19  	"github.com/go-acme/lego/v4/platform/wait"
  20  )
  21  
  22  type byType []acme.Challenge
  23  
  24  func (a byType) Len() int           { return len(a) }
  25  func (a byType) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
  26  func (a byType) Less(i, j int) bool { return a[i].Type > a[j].Type }
  27  
  28  type SolverManager struct {
  29  	core    *api.Core
  30  	solvers map[challenge.Type]solver
  31  }
  32  
  33  func NewSolversManager(core *api.Core) *SolverManager {
  34  	return &SolverManager{
  35  		solvers: map[challenge.Type]solver{},
  36  		core:    core,
  37  	}
  38  }
  39  
  40  // SetHTTP01Provider specifies a custom provider p that can solve the given HTTP-01 challenge.
  41  func (c *SolverManager) SetHTTP01Provider(p challenge.Provider, opts ...http01.ChallengeOption) error {
  42  	c.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p, opts...)
  43  	return nil
  44  }
  45  
  46  // SetTLSALPN01Provider specifies a custom provider p that can solve the given TLS-ALPN-01 challenge.
  47  func (c *SolverManager) SetTLSALPN01Provider(p challenge.Provider, opts ...tlsalpn01.ChallengeOption) error {
  48  	c.solvers[challenge.TLSALPN01] = tlsalpn01.NewChallenge(c.core, validate, p, opts...)
  49  	return nil
  50  }
  51  
  52  // SetDNS01Provider specifies a custom provider p that can solve the given DNS-01 challenge.
  53  func (c *SolverManager) SetDNS01Provider(p challenge.Provider, opts ...dns01.ChallengeOption) error {
  54  	c.solvers[challenge.DNS01] = dns01.NewChallenge(c.core, validate, p, opts...)
  55  	return nil
  56  }
  57  
  58  // Remove removes a challenge type from the available solvers.
  59  func (c *SolverManager) Remove(chlgType challenge.Type) {
  60  	delete(c.solvers, chlgType)
  61  }
  62  
  63  // Checks all challenges from the server in order and returns the first matching solver.
  64  func (c *SolverManager) chooseSolver(authz acme.Authorization) solver {
  65  	// Allow to have a deterministic challenge order
  66  	sort.Sort(byType(authz.Challenges))
  67  
  68  	domain := challenge.GetTargetedDomain(authz)
  69  	for _, chlg := range authz.Challenges {
  70  		if solvr, ok := c.solvers[challenge.Type(chlg.Type)]; ok {
  71  			log.Infof("[%s] acme: use %s solver", domain, chlg.Type)
  72  			return solvr
  73  		}
  74  
  75  		log.Infof("[%s] acme: Could not find solver for: %s", domain, chlg.Type)
  76  	}
  77  
  78  	return nil
  79  }
  80  
  81  func validate(core *api.Core, domain string, chlg acme.Challenge) error {
  82  	chlng, err := core.Challenges.New(chlg.URL)
  83  	if err != nil {
  84  		return fmt.Errorf("failed to initiate challenge: %w", err)
  85  	}
  86  
  87  	valid, err := checkChallengeStatus(chlng)
  88  	if err != nil {
  89  		return err
  90  	}
  91  
  92  	if valid {
  93  		log.Infof("[%s] The server validated our request", domain)
  94  		return nil
  95  	}
  96  
  97  	ra, err := strconv.Atoi(chlng.RetryAfter)
  98  	if err != nil {
  99  		// The ACME server MUST return a Retry-After.
 100  		// If it doesn't, we'll just poll hard.
 101  		// Boulder does not implement the ability to retry challenges or the Retry-After header.
 102  		// https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md#section-82
 103  		ra = 5
 104  	}
 105  
 106  	initialInterval := time.Duration(ra) * time.Second
 107  
 108  	ctx := context.Background()
 109  
 110  	bo := backoff.NewExponentialBackOff()
 111  	bo.InitialInterval = initialInterval
 112  	bo.MaxInterval = 10 * initialInterval
 113  
 114  	// After the path is sent, the ACME server will access our server.
 115  	// Repeatedly check the server for an updated status on our request.
 116  	operation := func() error {
 117  		authz, err := core.Authorizations.Get(chlng.AuthorizationURL)
 118  		if err != nil {
 119  			return backoff.Permanent(err)
 120  		}
 121  
 122  		valid, err := checkAuthorizationStatus(authz)
 123  		if err != nil {
 124  			return backoff.Permanent(err)
 125  		}
 126  
 127  		if valid {
 128  			log.Infof("[%s] The server validated our request", domain)
 129  			return nil
 130  		}
 131  
 132  		return fmt.Errorf("the server didn't respond to our request (status=%s)", authz.Status)
 133  	}
 134  
 135  	return wait.Retry(ctx, operation,
 136  		backoff.WithBackOff(bo),
 137  		backoff.WithMaxElapsedTime(100*initialInterval))
 138  }
 139  
 140  func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) {
 141  	switch chlng.Status {
 142  	case acme.StatusValid:
 143  		return true, nil
 144  	case acme.StatusPending, acme.StatusProcessing:
 145  		return false, nil
 146  	case acme.StatusInvalid:
 147  		return false, fmt.Errorf("invalid challenge: %w", chlng.Err())
 148  	default:
 149  		return false, fmt.Errorf("the server returned an unexpected challenge status: %s", chlng.Status)
 150  	}
 151  }
 152  
 153  func checkAuthorizationStatus(authz acme.Authorization) (bool, error) {
 154  	switch authz.Status {
 155  	case acme.StatusValid:
 156  		return true, nil
 157  	case acme.StatusPending, acme.StatusProcessing:
 158  		return false, nil
 159  	case acme.StatusDeactivated, acme.StatusExpired, acme.StatusRevoked:
 160  		return false, fmt.Errorf("the authorization state %s", authz.Status)
 161  	case acme.StatusInvalid:
 162  		for _, chlg := range authz.Challenges {
 163  			if chlg.Status == acme.StatusInvalid && chlg.Error != nil {
 164  				return false, fmt.Errorf("invalid authorization: %w", chlg.Err())
 165  			}
 166  		}
 167  
 168  		return false, errors.New("invalid authorization")
 169  	default:
 170  		return false, fmt.Errorf("the server returned an unexpected authorization status: %s", authz.Status)
 171  	}
 172  }
 173