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