prober.go raw

   1  package resolver
   2  
   3  import (
   4  	"fmt"
   5  	"time"
   6  
   7  	"github.com/go-acme/lego/v4/acme"
   8  	"github.com/go-acme/lego/v4/challenge"
   9  	"github.com/go-acme/lego/v4/log"
  10  )
  11  
  12  // Interface for all challenge solvers to implement.
  13  type solver interface {
  14  	Solve(authorization acme.Authorization) error
  15  }
  16  
  17  // Interface for challenges like dns, where we can set a record in advance for ALL challenges.
  18  // This saves quite a bit of time vs creating the records and solving them serially.
  19  type preSolver interface {
  20  	PreSolve(authorization acme.Authorization) error
  21  }
  22  
  23  // Interface for challenges like dns, where we can solve all the challenges before to delete them.
  24  type cleanup interface {
  25  	CleanUp(authorization acme.Authorization) error
  26  }
  27  
  28  type sequential interface {
  29  	Sequential() (bool, time.Duration)
  30  }
  31  
  32  // an authz with the solver we have chosen and the index of the challenge associated with it.
  33  type selectedAuthSolver struct {
  34  	authz  acme.Authorization
  35  	solver solver
  36  }
  37  
  38  type Prober struct {
  39  	solverManager *SolverManager
  40  }
  41  
  42  func NewProber(solverManager *SolverManager) *Prober {
  43  	return &Prober{
  44  		solverManager: solverManager,
  45  	}
  46  }
  47  
  48  // Solve Looks through the challenge combinations to find a solvable match.
  49  // Then solves the challenges in series and returns.
  50  func (p *Prober) Solve(authorizations []acme.Authorization) error {
  51  	failures := make(obtainError)
  52  
  53  	var (
  54  		authSolvers           []*selectedAuthSolver
  55  		authSolversSequential []*selectedAuthSolver
  56  	)
  57  
  58  	// Loop through the resources, basically through the domains.
  59  	// First pass just selects a solver for each authz.
  60  
  61  	for _, authz := range authorizations {
  62  		domain := challenge.GetTargetedDomain(authz)
  63  		if authz.Status == acme.StatusValid {
  64  			// Boulder might recycle recent validated authz (see issue #267)
  65  			log.Infof("[%s] acme: authorization already valid; skipping challenge", domain)
  66  			continue
  67  		}
  68  
  69  		if solvr := p.solverManager.chooseSolver(authz); solvr != nil {
  70  			authSolver := &selectedAuthSolver{authz: authz, solver: solvr}
  71  
  72  			switch s := solvr.(type) {
  73  			case sequential:
  74  				if ok, _ := s.Sequential(); ok {
  75  					authSolversSequential = append(authSolversSequential, authSolver)
  76  				} else {
  77  					authSolvers = append(authSolvers, authSolver)
  78  				}
  79  			default:
  80  				authSolvers = append(authSolvers, authSolver)
  81  			}
  82  		} else {
  83  			failures[domain] = fmt.Errorf("[%s] acme: could not determine solvers", domain)
  84  		}
  85  	}
  86  
  87  	parallelSolve(authSolvers, failures)
  88  
  89  	sequentialSolve(authSolversSequential, failures)
  90  
  91  	// Be careful not to return an empty failures map,
  92  	// for even an empty obtainError is a non-nil error value
  93  	if len(failures) > 0 {
  94  		return failures
  95  	}
  96  
  97  	return nil
  98  }
  99  
 100  func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
 101  	for i, authSolver := range authSolvers {
 102  		// Submit the challenge
 103  		domain := challenge.GetTargetedDomain(authSolver.authz)
 104  
 105  		if solvr, ok := authSolver.solver.(preSolver); ok {
 106  			err := solvr.PreSolve(authSolver.authz)
 107  			if err != nil {
 108  				failures[domain] = err
 109  
 110  				cleanUp(authSolver.solver, authSolver.authz)
 111  
 112  				continue
 113  			}
 114  		}
 115  
 116  		// Solve challenge
 117  		err := authSolver.solver.Solve(authSolver.authz)
 118  		if err != nil {
 119  			failures[domain] = err
 120  
 121  			cleanUp(authSolver.solver, authSolver.authz)
 122  
 123  			continue
 124  		}
 125  
 126  		// Clean challenge
 127  		cleanUp(authSolver.solver, authSolver.authz)
 128  
 129  		if len(authSolvers)-1 > i {
 130  			solvr := authSolver.solver.(sequential)
 131  			_, interval := solvr.Sequential()
 132  			log.Infof("sequence: wait for %s", interval)
 133  			time.Sleep(interval)
 134  		}
 135  	}
 136  }
 137  
 138  func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
 139  	// For all valid preSolvers, first submit the challenges, so they have max time to propagate
 140  	for _, authSolver := range authSolvers {
 141  		authz := authSolver.authz
 142  		if solvr, ok := authSolver.solver.(preSolver); ok {
 143  			err := solvr.PreSolve(authz)
 144  			if err != nil {
 145  				failures[challenge.GetTargetedDomain(authz)] = err
 146  			}
 147  		}
 148  	}
 149  
 150  	defer func() {
 151  		// Clean all created TXT records
 152  		for _, authSolver := range authSolvers {
 153  			cleanUp(authSolver.solver, authSolver.authz)
 154  		}
 155  	}()
 156  
 157  	// Finally solve all challenges for real
 158  	for _, authSolver := range authSolvers {
 159  		authz := authSolver.authz
 160  
 161  		domain := challenge.GetTargetedDomain(authz)
 162  		if failures[domain] != nil {
 163  			// already failed in previous loop
 164  			continue
 165  		}
 166  
 167  		err := authSolver.solver.Solve(authz)
 168  		if err != nil {
 169  			failures[domain] = err
 170  		}
 171  	}
 172  }
 173  
 174  func cleanUp(solvr solver, authz acme.Authorization) {
 175  	if solvr, ok := solvr.(cleanup); ok {
 176  		domain := challenge.GetTargetedDomain(authz)
 177  
 178  		err := solvr.CleanUp(authz)
 179  		if err != nil {
 180  			log.Warnf("[%s] acme: cleaning up failed: %v ", domain, err)
 181  		}
 182  	}
 183  }
 184