httpreq.go raw
1 // Package httpreq implements a DNS provider for solving the DNS-01 challenge through an HTTP server.
2 package httpreq
3
4 import (
5 "bytes"
6 "context"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "net/http"
11 "net/url"
12 "time"
13
14 "github.com/go-acme/lego/v4/challenge"
15 "github.com/go-acme/lego/v4/challenge/dns01"
16 "github.com/go-acme/lego/v4/platform/config/env"
17 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
18 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
19 )
20
21 // Environment variables names.
22 const (
23 envNamespace = "HTTPREQ_"
24
25 EnvEndpoint = envNamespace + "ENDPOINT"
26 EnvMode = envNamespace + "MODE"
27 EnvUsername = envNamespace + "USERNAME"
28 EnvPassword = envNamespace + "PASSWORD"
29
30 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
31 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
32 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
33 )
34
35 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
36
37 type message struct {
38 FQDN string `json:"fqdn"`
39 Value string `json:"value"`
40 }
41
42 type messageRaw struct {
43 Domain string `json:"domain"`
44 Token string `json:"token"`
45 KeyAuth string `json:"keyAuth"`
46 }
47
48 // Config is used to configure the creation of the DNSProvider.
49 type Config struct {
50 Endpoint *url.URL
51 Mode string
52 Username string
53 Password string
54 PropagationTimeout time.Duration
55 PollingInterval time.Duration
56 HTTPClient *http.Client
57 }
58
59 // NewDefaultConfig returns a default configuration for the DNSProvider.
60 func NewDefaultConfig() *Config {
61 return &Config{
62 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
63 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
64 HTTPClient: &http.Client{
65 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
66 },
67 }
68 }
69
70 // DNSProvider implements the challenge.Provider interface.
71 type DNSProvider struct {
72 config *Config
73 }
74
75 // NewDNSProvider returns a DNSProvider instance.
76 func NewDNSProvider() (*DNSProvider, error) {
77 values, err := env.Get(EnvEndpoint)
78 if err != nil {
79 return nil, fmt.Errorf("httpreq: %w", err)
80 }
81
82 endpoint, err := url.Parse(values[EnvEndpoint])
83 if err != nil {
84 return nil, fmt.Errorf("httpreq: %w", err)
85 }
86
87 config := NewDefaultConfig()
88 config.Mode = env.GetOrFile(EnvMode)
89 config.Username = env.GetOrFile(EnvUsername)
90 config.Password = env.GetOrFile(EnvPassword)
91 config.Endpoint = endpoint
92
93 return NewDNSProviderConfig(config)
94 }
95
96 // NewDNSProviderConfig return a DNSProvider.
97 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
98 if config == nil {
99 return nil, errors.New("httpreq: the configuration of the DNS provider is nil")
100 }
101
102 if config.Endpoint == nil {
103 return nil, errors.New("httpreq: the endpoint is missing")
104 }
105
106 config.HTTPClient = clientdebug.Wrap(config.HTTPClient)
107
108 return &DNSProvider{config: config}, nil
109 }
110
111 // Timeout returns the timeout and interval to use when checking for DNS propagation.
112 // Adjusting here to cope with spikes in propagation times.
113 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
114 return d.config.PropagationTimeout, d.config.PollingInterval
115 }
116
117 // Present creates a TXT record to fulfill the dns-01 challenge.
118 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
119 ctx := context.Background()
120
121 if d.config.Mode == "RAW" {
122 msg := &messageRaw{
123 Domain: domain,
124 Token: token,
125 KeyAuth: keyAuth,
126 }
127
128 err := d.doPost(ctx, "/present", msg)
129 if err != nil {
130 return fmt.Errorf("httpreq: %w", err)
131 }
132
133 return nil
134 }
135
136 info := dns01.GetChallengeInfo(domain, keyAuth)
137 msg := &message{
138 FQDN: info.EffectiveFQDN,
139 Value: info.Value,
140 }
141
142 err := d.doPost(ctx, "/present", msg)
143 if err != nil {
144 return fmt.Errorf("httpreq: %w", err)
145 }
146
147 return nil
148 }
149
150 // CleanUp removes the TXT record matching the specified parameters.
151 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
152 ctx := context.Background()
153
154 if d.config.Mode == "RAW" {
155 msg := &messageRaw{
156 Domain: domain,
157 Token: token,
158 KeyAuth: keyAuth,
159 }
160
161 err := d.doPost(ctx, "/cleanup", msg)
162 if err != nil {
163 return fmt.Errorf("httpreq: %w", err)
164 }
165
166 return nil
167 }
168
169 info := dns01.GetChallengeInfo(domain, keyAuth)
170 msg := &message{
171 FQDN: info.EffectiveFQDN,
172 Value: info.Value,
173 }
174
175 err := d.doPost(ctx, "/cleanup", msg)
176 if err != nil {
177 return fmt.Errorf("httpreq: %w", err)
178 }
179
180 return nil
181 }
182
183 func (d *DNSProvider) doPost(ctx context.Context, uri string, msg any) error {
184 reqBody := new(bytes.Buffer)
185
186 err := json.NewEncoder(reqBody).Encode(msg)
187 if err != nil {
188 return fmt.Errorf("failed to create request JSON body: %w", err)
189 }
190
191 endpoint := d.config.Endpoint.JoinPath(uri)
192
193 req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), reqBody)
194 if err != nil {
195 return fmt.Errorf("unable to create request: %w", err)
196 }
197
198 req.Header.Set("Accept", "application/json")
199 req.Header.Set("Content-Type", "application/json")
200
201 if d.config.Username != "" && d.config.Password != "" {
202 req.SetBasicAuth(d.config.Username, d.config.Password)
203 }
204
205 resp, err := d.config.HTTPClient.Do(req)
206 if err != nil {
207 return errutils.NewHTTPDoError(req, err)
208 }
209
210 defer func() { _ = resp.Body.Close() }()
211
212 if resp.StatusCode/100 != 2 {
213 return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
214 }
215
216 return nil
217 }
218