package resolver import ( "fmt" "time" "github.com/go-acme/lego/v3/acme" "github.com/go-acme/lego/v3/challenge" "github.com/go-acme/lego/v3/log" ) // Interface for all challenge solvers to implement. type solver interface { Solve(authorization acme.Authorization) error } // Interface for challenges like dns, where we can set a record in advance for ALL challenges. // This saves quite a bit of time vs creating the records and solving them serially. type preSolver interface { PreSolve(authorization acme.Authorization) error } // Interface for challenges like dns, where we can solve all the challenges before to delete them. type cleanup interface { CleanUp(authorization acme.Authorization) error } type sequential interface { Sequential() (bool, time.Duration) } // an authz with the solver we have chosen and the index of the challenge associated with it type selectedAuthSolver struct { authz acme.Authorization solver solver } type Prober struct { solverManager *SolverManager } func NewProber(solverManager *SolverManager) *Prober { return &Prober{ solverManager: solverManager, } } // Solve Looks through the challenge combinations to find a solvable match. // Then solves the challenges in series and returns. func (p *Prober) Solve(authorizations []acme.Authorization) error { failures := make(obtainError) var authSolvers []*selectedAuthSolver var authSolversSequential []*selectedAuthSolver // Loop through the resources, basically through the domains. // First pass just selects a solver for each authz. for _, authz := range authorizations { domain := challenge.GetTargetedDomain(authz) if authz.Status == acme.StatusValid { // Boulder might recycle recent validated authz (see issue #267) log.Infof("[%s] acme: authorization already valid; skipping challenge", domain) continue } if solvr := p.solverManager.chooseSolver(authz); solvr != nil { authSolver := &selectedAuthSolver{authz: authz, solver: solvr} switch s := solvr.(type) { case sequential: if ok, _ := s.Sequential(); ok { authSolversSequential = append(authSolversSequential, authSolver) } else { authSolvers = append(authSolvers, authSolver) } default: authSolvers = append(authSolvers, authSolver) } } else { failures[domain] = fmt.Errorf("[%s] acme: could not determine solvers", domain) } } parallelSolve(authSolvers, failures) sequentialSolve(authSolversSequential, failures) // Be careful not to return an empty failures map, // for even an empty obtainError is a non-nil error value if len(failures) > 0 { return failures } return nil } func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) { for i, authSolver := range authSolvers { // Submit the challenge domain := challenge.GetTargetedDomain(authSolver.authz) if solvr, ok := authSolver.solver.(preSolver); ok { err := solvr.PreSolve(authSolver.authz) if err != nil { failures[domain] = err cleanUp(authSolver.solver, authSolver.authz) continue } } // Solve challenge err := authSolver.solver.Solve(authSolver.authz) if err != nil { failures[domain] = err cleanUp(authSolver.solver, authSolver.authz) continue } // Clean challenge cleanUp(authSolver.solver, authSolver.authz) if len(authSolvers)-1 > i { solvr := authSolver.solver.(sequential) _, interval := solvr.Sequential() log.Infof("sequence: wait for %s", interval) time.Sleep(interval) } } } func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) { // For all valid preSolvers, first submit the challenges so they have max time to propagate for _, authSolver := range authSolvers { authz := authSolver.authz if solvr, ok := authSolver.solver.(preSolver); ok { err := solvr.PreSolve(authz) if err != nil { failures[challenge.GetTargetedDomain(authz)] = err } } } defer func() { // Clean all created TXT records for _, authSolver := range authSolvers { cleanUp(authSolver.solver, authSolver.authz) } }() // Finally solve all challenges for real for _, authSolver := range authSolvers { authz := authSolver.authz domain := challenge.GetTargetedDomain(authz) if failures[domain] != nil { // already failed in previous loop continue } err := authSolver.solver.Solve(authz) if err != nil { failures[domain] = err } } } func cleanUp(solvr solver, authz acme.Authorization) { if solvr, ok := solvr.(cleanup); ok { domain := challenge.GetTargetedDomain(authz) err := solvr.CleanUp(authz) if err != nil { log.Warnf("[%s] acme: cleaning up failed: %v ", domain, err) } } }