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