/
http.go
691 lines (622 loc) · 25.7 KB
/
http.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
package va
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"unicode"
"github.com/letsencrypt/boulder/bdns"
"github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/iana"
"github.com/letsencrypt/boulder/identifier"
)
const (
// maxRedirect is the maximum number of redirects the VA will follow
// processing an HTTP-01 challenge.
maxRedirect = 10
// maxResponseSize holds the maximum number of bytes that will be read from an
// HTTP-01 challenge response. The expected payload should be ~87 bytes. Since
// it may be padded by whitespace which we previously allowed accept up to 128
// bytes before rejecting a response (32 byte b64 encoded token + . + 32 byte
// b64 encoded key fingerprint).
maxResponseSize = 128
// maxPathSize is the maximum number of bytes we will accept in the path of a
// redirect URL.
maxPathSize = 2000
)
// preresolvedDialer is a struct type that provides a DialContext function which
// will connect to the provided IP and port instead of letting DNS resolve
// The hostname of the preresolvedDialer is used to ensure the dial only completes
// using the pre-resolved IP/port when used for the correct host.
type preresolvedDialer struct {
ip net.IP
port int
hostname string
timeout time.Duration
}
// a dialerMismatchError is produced when a preresolvedDialer is used to dial
// a host other than the dialer's specified hostname.
type dialerMismatchError struct {
// The original dialer information
dialerHost string
dialerIP string
dialerPort int
// The host that the dialer was incorrectly used with
host string
}
func (e *dialerMismatchError) Error() string {
return fmt.Sprintf(
"preresolvedDialer mismatch: dialer is for %q (ip: %q port: %d) not %q",
e.dialerHost, e.dialerIP, e.dialerPort, e.host)
}
// DialContext for a preresolvedDialer shaves 10ms off of the context it was
// given before calling the default transport DialContext using the pre-resolved
// IP and port as the host. If the original host being dialed by DialContext
// does not match the expected hostname in the preresolvedDialer an error will
// be returned instead. This helps prevents a bug that might use
// a preresolvedDialer for the wrong host.
//
// Shaving the context helps us be able to differentiate between timeouts during
// connect and timeouts after connect.
//
// Using preresolved information for the host argument given to the real
// transport dial lets us have fine grained control over IP address resolution for
// domain names.
func (d *preresolvedDialer) DialContext(
ctx context.Context,
network,
origAddr string) (net.Conn, error) {
deadline, ok := ctx.Deadline()
if !ok {
// Shouldn't happen: All requests should have a deadline by this point.
deadline = time.Now().Add(100 * time.Second)
} else {
// Set the context deadline slightly shorter than the HTTP deadline, so we
// get a useful error rather than a generic "deadline exceeded" error. This
// lets us give a more specific error to the subscriber.
deadline = deadline.Add(-10 * time.Millisecond)
}
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
// NOTE(@cpu): I don't capture and check the origPort here because using
// `net.SplitHostPort` and also supporting the va's custom httpPort and
// httpsPort is cumbersome. The initial origAddr may be "example.com:80"
// if the URL used for the dial input was "http://example.com" without an
// explicit port. Checking for equality here will fail unless we add
// special case logic for converting 80/443 -> httpPort/httpsPort when
// configured. This seems more likely to cause bugs than catch them so I'm
// ignoring this for now. In the future if we remove the httpPort/httpsPort
// (we should!) we can also easily enforce that the preresolved dialer port
// matches expected here.
origHost, _, err := net.SplitHostPort(origAddr)
if err != nil {
return nil, err
}
// If the hostname we're dialing isn't equal to the hostname the dialer was
// constructed for then a bug has occurred where we've mismatched the
// preresolved dialer.
if origHost != d.hostname {
return nil, &dialerMismatchError{
dialerHost: d.hostname,
dialerIP: d.ip.String(),
dialerPort: d.port,
host: origHost,
}
}
// Make a new dial address using the pre-resolved IP and port.
targetAddr := net.JoinHostPort(d.ip.String(), strconv.Itoa(d.port))
// Create a throw-away dialer using default values and the dialer timeout
// (populated from the VA singleDialTimeout).
throwAwayDialer := &net.Dialer{
Timeout: d.timeout,
// Default KeepAlive - see Golang src/net/http/transport.go DefaultTransport
KeepAlive: 30 * time.Second,
}
return throwAwayDialer.DialContext(ctx, network, targetAddr)
}
// a dialerFunc meets the function signature requirements of
// a http.Transport.DialContext handler.
type dialerFunc func(ctx context.Context, network, addr string) (net.Conn, error)
// httpTransport constructs a HTTP Transport with settings appropriate for
// HTTP-01 validation. The provided dialerFunc is used as the Transport's
// DialContext handler.
func httpTransport(df dialerFunc) *http.Transport {
return &http.Transport{
DialContext: df,
// We are talking to a client that does not yet have a certificate,
// so we accept a temporary, invalid one.
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
// We don't expect to make multiple requests to a client, so close
// connection immediately.
DisableKeepAlives: true,
// We don't want idle connections, but 0 means "unlimited," so we pick 1.
MaxIdleConns: 1,
IdleConnTimeout: time.Second,
TLSHandshakeTimeout: 10 * time.Second,
}
}
// httpValidationTarget bundles all of the information needed to make an HTTP-01
// validation request against a target.
type httpValidationTarget struct {
// the hostname being validated
host string
// the port for the validation request
port int
// the path for the validation request
path string
// query data for validation request (potentially populated when
// following redirects)
query string
// all of the IP addresses available for the host
available []net.IP
// the IP addresses that were tried for validation previously that were cycled
// out of cur by calls to nextIP()
tried []net.IP
// the IP addresses that will be drawn from by calls to nextIP() to set curIP
next []net.IP
// the current IP address being used for validation (if any)
cur net.IP
// the DNS resolver(s) that will attempt to fulfill the validation request
resolvers bdns.ResolverAddrs
}
// nextIP changes the cur IP by removing the first entry from the next slice and
// setting it to cur. If cur was previously set the value will be added to the
// tried slice to keep track of IPs that were previously used. If nextIP() is
// called but vt.next is empty an error is returned.
func (vt *httpValidationTarget) nextIP() error {
if len(vt.next) == 0 {
return fmt.Errorf(
"host %q has no IP addresses remaining to use",
vt.host)
}
vt.tried = append(vt.tried, vt.cur)
vt.cur = vt.next[0]
vt.next = vt.next[1:]
return nil
}
// ip returns the current *net.IP for the validation target. It may return nil
// if all possible IPs have been expended by calls to nextIP.
func (vt *httpValidationTarget) ip() net.IP {
return vt.cur
}
// newHTTPValidationTarget creates a httpValidationTarget for the given host,
// port, and path. This involves querying DNS for the IP addresses for the host.
// An error is returned if there are no usable IP addresses or if the DNS
// lookups fail.
func (va *ValidationAuthorityImpl) newHTTPValidationTarget(
ctx context.Context,
host string,
port int,
path string,
query string) (*httpValidationTarget, error) {
// Resolve IP addresses for the hostname
addrs, resolvers, err := va.getAddrs(ctx, host)
if err != nil {
return nil, err
}
target := &httpValidationTarget{
host: host,
port: port,
path: path,
query: query,
available: addrs,
resolvers: resolvers,
}
// Separate the addresses into the available v4 and v6 addresses
v4Addrs, v6Addrs := availableAddresses(addrs)
hasV6Addrs := len(v6Addrs) > 0
hasV4Addrs := len(v4Addrs) > 0
if !hasV6Addrs && !hasV4Addrs {
// If there are no v6 addrs and no v4addrs there was a bug with getAddrs or
// availableAddresses and we need to return an error.
return nil, fmt.Errorf("host %q has no IPv4 or IPv6 addresses", host)
} else if !hasV6Addrs && hasV4Addrs {
// If there are no v6 addrs and there are v4 addrs then use the first v4
// address. There's no fallback address.
target.next = []net.IP{v4Addrs[0]}
} else if hasV6Addrs && hasV4Addrs {
// If there are both v6 addrs and v4 addrs then use the first v6 address and
// fallback with the first v4 address.
target.next = []net.IP{v6Addrs[0], v4Addrs[0]}
} else if hasV6Addrs && !hasV4Addrs {
// If there are just v6 addrs then use the first v6 address. There's no
// fallback address.
target.next = []net.IP{v6Addrs[0]}
}
// Advance the target using nextIP to populate the cur IP before returning
_ = target.nextIP()
return target, nil
}
// extractRequestTarget extracts the hostname and port specified in the provided
// HTTP redirect request. If the request's URL's protocol schema is not HTTP or
// HTTPS an error is returned. If an explicit port is specified in the request's
// URL and it isn't the VA's HTTP or HTTPS port, an error is returned. If the
// request's URL's Host is a bare IPv4 or IPv6 address and not a domain name an
// error is returned.
func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (string, int, error) {
// A nil request is certainly not a valid redirect and has no port to extract.
if req == nil {
return "", 0, fmt.Errorf("redirect HTTP request was nil")
}
reqScheme := req.URL.Scheme
// The redirect request must use HTTP or HTTPs protocol schemes regardless of the port..
if reqScheme != "http" && reqScheme != "https" {
return "", 0, berrors.ConnectionFailureError(
"Invalid protocol scheme in redirect target. "+
`Only "http" and "https" protocol schemes are supported, not %q`, reqScheme)
}
// Try and split an explicit port number from the request URL host. If there is
// one we need to make sure its a valid port. If there isn't one we need to
// pick the port based on the reqScheme default port.
reqHost := req.URL.Host
var reqPort int
if h, p, err := net.SplitHostPort(reqHost); err == nil {
reqHost = h
reqPort, err = strconv.Atoi(p)
if err != nil {
return "", 0, err
}
// The explicit port must match the VA's configured HTTP or HTTPS port.
if reqPort != va.httpPort && reqPort != va.httpsPort {
return "", 0, berrors.ConnectionFailureError(
"Invalid port in redirect target. Only ports %d and %d are supported, not %d",
va.httpPort, va.httpsPort, reqPort)
}
} else if reqScheme == "http" {
reqPort = va.httpPort
} else if reqScheme == "https" {
reqPort = va.httpsPort
} else {
// This shouldn't happen but defensively return an internal server error in
// case it does.
return "", 0, fmt.Errorf("unable to determine redirect HTTP request port")
}
if reqHost == "" {
return "", 0, berrors.ConnectionFailureError("Invalid empty hostname in redirect target")
}
// Check that the request host isn't a bare IP address. We only follow
// redirects to hostnames.
if net.ParseIP(reqHost) != nil {
return "", 0, berrors.ConnectionFailureError("Invalid host in redirect target %q. Only domain names are supported, not IP addresses", reqHost)
}
// Often folks will misconfigure their webserver to send an HTTP redirect
// missing a `/' between the FQDN and the path. E.g. in Apache using:
// Redirect / https://bad-redirect.org
// Instead of
// Redirect / https://bad-redirect.org/
// Will produce an invalid HTTP-01 redirect target like:
// https://bad-redirect.org.well-known/acme-challenge/xxxx
// This happens frequently enough we want to return a distinct error message
// for this case by detecting the reqHost ending in ".well-known".
if strings.HasSuffix(reqHost, ".well-known") {
return "", 0, berrors.ConnectionFailureError(
"Invalid host in redirect target %q. Check webserver config for missing '/' in redirect target.",
reqHost,
)
}
if _, err := iana.ExtractSuffix(reqHost); err != nil {
return "", 0, berrors.ConnectionFailureError("Invalid hostname in redirect target, must end in IANA registered TLD")
}
return reqHost, reqPort, nil
}
// setupHTTPValidation sets up a preresolvedDialer and a validation record for
// the given request URL and httpValidationTarget. If the req URL is empty, or
// the validation target is nil or has no available IP addresses, an error will
// be returned.
func (va *ValidationAuthorityImpl) setupHTTPValidation(
reqURL string,
target *httpValidationTarget) (*preresolvedDialer, core.ValidationRecord, error) {
if reqURL == "" {
return nil,
core.ValidationRecord{},
fmt.Errorf("reqURL can not be nil")
}
if target == nil {
// This is the only case where returning an empty validation record makes
// sense - we can't construct a better one, something has gone quite wrong.
return nil,
core.ValidationRecord{},
fmt.Errorf("httpValidationTarget can not be nil")
}
// Construct a base validation record with the validation target's
// information.
record := core.ValidationRecord{
Hostname: target.host,
Port: strconv.Itoa(target.port),
AddressesResolved: target.available,
URL: reqURL,
ResolverAddrs: target.resolvers,
}
// Get the target IP to build a preresolved dialer with
targetIP := target.ip()
if targetIP == nil {
return nil,
record,
fmt.Errorf(
"host %q has no IP addresses remaining to use",
target.host)
}
record.AddressUsed = targetIP
dialer := &preresolvedDialer{
ip: targetIP,
port: target.port,
hostname: target.host,
timeout: va.singleDialTimeout,
}
return dialer, record, nil
}
// fetchHTTP invokes processHTTPValidation and if an error result is
// returned, converts it to a problem. Otherwise the results from
// processHTTPValidation are returned.
func (va *ValidationAuthorityImpl) fetchHTTP(
ctx context.Context,
host string,
path string) ([]byte, []core.ValidationRecord, error) {
body, records, err := va.processHTTPValidation(ctx, host, path)
if err != nil {
return body, records, err
}
return body, records, nil
}
// fallbackErr returns true only for net.OpError instances where the op is equal
// to "dial", or url.Error instances wrapping such an error. fallbackErr returns
// false for all other errors. By policy, only dial errors (not read or write
// errors) are eligible for fallback from an IPv6 to an IPv4 address.
func fallbackErr(err error) bool {
// Err shouldn't ever be nil if we're considering it for fallback
if err == nil {
return false
}
// Net OpErrors are fallback errs only if the operation was a "dial"
// All other errs are not fallback errs
var netOpError *net.OpError
return errors.As(err, &netOpError) && netOpError.Op == "dial"
}
// processHTTPValidation performs an HTTP validation for the given host, port
// and path. If successful the body of the HTTP response is returned along with
// the validation records created during the validation. If not successful
// a non-nil error and potentially some ValidationRecords are returned.
func (va *ValidationAuthorityImpl) processHTTPValidation(
ctx context.Context,
host string,
path string) ([]byte, []core.ValidationRecord, error) {
// Create a target for the host, port and path with no query parameters
target, err := va.newHTTPValidationTarget(ctx, host, va.httpPort, path, "")
if err != nil {
return nil, nil, err
}
// newIPError implements the error interface. It wraps an error and the IP
// of the remote host in an IPError so we can display the IP in the problem
// details returned to the client.
newIPError := func(target *httpValidationTarget, err error) error {
return ipError{ip: target.cur, err: err}
}
// Create an initial GET Request
initialURL := url.URL{
Scheme: "http",
Host: host,
Path: path,
}
initialReq, err := http.NewRequest("GET", initialURL.String(), nil)
if err != nil {
return nil, nil, newIPError(target, err)
}
// Add a context to the request. Shave some time from the
// overall context deadline so that we are not racing with gRPC when the
// HTTP server is timing out. This avoids returning ServerInternal
// errors when we should be returning Connection errors. This may fix a flaky
// integration test: https://github.com/letsencrypt/boulder/issues/4087
// Note: The gRPC interceptor in grpc/interceptors.go already shaves some time
// off RPCs, but this takes off additional time because HTTP-related timeouts
// are so common (and because it might fix a flaky build).
deadline, ok := ctx.Deadline()
if !ok {
return nil, nil, fmt.Errorf("processHTTPValidation had no deadline")
} else {
deadline = deadline.Add(-200 * time.Millisecond)
}
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
initialReq = initialReq.WithContext(ctx)
if va.userAgent != "" {
initialReq.Header.Set("User-Agent", va.userAgent)
}
// Some of our users use mod_security. Mod_security sees a lack of Accept
// headers as bot behavior and rejects requests. While this is a bug in
// mod_security's rules (given that the HTTP specs disagree with that
// requirement), we add the Accept header now in order to fix our
// mod_security users' mysterious breakages. See
// <https://github.com/SpiderLabs/owasp-modsecurity-crs/issues/265> and
// <https://github.com/letsencrypt/boulder/issues/1019>. This was done
// because it's a one-line fix with no downside. We're not likely to want to
// do many more things to satisfy misunderstandings around HTTP.
initialReq.Header.Set("Accept", "*/*")
// Set up the initial validation request and a base validation record
dialer, baseRecord, err := va.setupHTTPValidation(initialReq.URL.String(), target)
if err != nil {
return nil, []core.ValidationRecord{}, newIPError(target, err)
}
// Build a transport for this validation that will use the preresolvedDialer's
// DialContext function
transport := httpTransport(dialer.DialContext)
va.log.AuditInfof("Attempting to validate HTTP-01 for %q with GET to %q",
initialReq.Host, initialReq.URL.String())
// Create a closure around records & numRedirects we can use with a HTTP
// client to process redirects per our own policy (e.g. resolving IP
// addresses explicitly, not following redirects to ports != [80,443], etc)
records := []core.ValidationRecord{baseRecord}
numRedirects := 0
processRedirect := func(req *http.Request, via []*http.Request) error {
va.log.Debugf("processing a HTTP redirect from the server to %q", req.URL.String())
// Only process up to maxRedirect redirects
if numRedirects > maxRedirect {
return berrors.ConnectionFailureError("Too many redirects")
}
numRedirects++
va.metrics.http01Redirects.Inc()
// If TLS was used, record the negotiated key exchange mechanism in the most
// recent validationRecord.
// TODO(#7321): Remove this when we have collected enough data.
if req.Response.TLS != nil {
records[len(records)-1].UsedRSAKEX = usedRSAKEX(req.Response.TLS.CipherSuite)
}
if req.Response.TLS != nil && req.Response.TLS.Version < tls.VersionTLS12 {
return berrors.ConnectionFailureError(
"validation attempt was redirected to an HTTPS server that doesn't " +
"support TLSv1.2 or better. See " +
"https://community.letsencrypt.org/t/rejecting-sha-1-csrs-and-validation-using-tls-1-0-1-1-urls/175144")
}
// If the response contains an HTTP 303 or any other forbidden redirect,
// do not follow it. The four allowed redirect status codes are defined
// explicitly in BRs Section 3.2.2.4.19. Although the go stdlib currently
// limits redirects to a set of status codes with only one additional
// entry (303), we capture the full list of allowed codes here in case the
// go stdlib expands the set of redirects it follows in the future.
acceptableRedirects := map[int]struct{}{
301: {}, 302: {}, 307: {}, 308: {},
}
if _, present := acceptableRedirects[req.Response.StatusCode]; !present {
return berrors.ConnectionFailureError("received disallowed redirect status code")
}
// Lowercase the redirect host immediately, as the dialer and redirect
// validation expect it to have been lowercased already.
req.URL.Host = strings.ToLower(req.URL.Host)
// Extract the redirect target's host and port. This will return an error if
// the redirect request scheme, host or port is not acceptable.
redirHost, redirPort, err := va.extractRequestTarget(req)
if err != nil {
return err
}
redirPath := req.URL.Path
if len(redirPath) > maxPathSize {
return berrors.ConnectionFailureError("Redirect target too long")
}
// If the redirect URL has query parameters we need to preserve
// those in the redirect path
redirQuery := ""
if req.URL.RawQuery != "" {
redirQuery = req.URL.RawQuery
}
// Check for a redirect loop. If any URL is found twice before the
// redirect limit, return error.
for _, record := range records {
if req.URL.String() == record.URL {
return berrors.ConnectionFailureError("Redirect loop detected")
}
}
// Create a validation target for the redirect host. This will resolve IP
// addresses for the host explicitly.
redirTarget, err := va.newHTTPValidationTarget(ctx, redirHost, redirPort, redirPath, redirQuery)
if err != nil {
return err
}
// Setup validation for the target. This will produce a preresolved dialer we can
// assign to the client transport in order to connect to the redirect target using
// the IP address we selected.
redirDialer, redirRecord, err := va.setupHTTPValidation(req.URL.String(), redirTarget)
records = append(records, redirRecord)
if err != nil {
return err
}
va.log.Debugf("following redirect to host %q url %q", req.Host, req.URL.String())
// Replace the transport's DialContext with the new preresolvedDialer for
// the redirect.
transport.DialContext = redirDialer.DialContext
return nil
}
// Create a new HTTP client configured to use the customized transport and
// to check HTTP redirects encountered with processRedirect
client := http.Client{
Transport: transport,
CheckRedirect: processRedirect,
}
// Make the initial validation request. This may result in redirects being
// followed.
httpResponse, err := client.Do(initialReq)
// If there was an error and its a kind of error we consider a fallback error,
// then try to fallback.
if err != nil && fallbackErr(err) {
// Try to advance to another IP. If there was an error advancing we don't
// have a fallback address to use and must return the original error.
advanceTargetIPErr := target.nextIP()
if advanceTargetIPErr != nil {
return nil, records, newIPError(target, err)
}
// setup another validation to retry the target with the new IP and append
// the retry record.
retryDialer, retryRecord, err := va.setupHTTPValidation(initialReq.URL.String(), target)
records = append(records, retryRecord)
if err != nil {
return nil, records, newIPError(target, err)
}
va.metrics.http01Fallbacks.Inc()
// Replace the transport's dialer with the preresolvedDialer for the retry
// host.
transport.DialContext = retryDialer.DialContext
// Perform the retry
httpResponse, err = client.Do(initialReq)
// If the retry still failed there isn't anything more to do, return the
// error immediately.
if err != nil {
return nil, records, newIPError(target, err)
}
} else if err != nil {
// if the error was not a fallbackErr then return immediately.
return nil, records, newIPError(target, err)
}
if httpResponse.StatusCode != 200 {
return nil, records, newIPError(target, berrors.UnauthorizedError("Invalid response from %s: %d",
records[len(records)-1].URL, httpResponse.StatusCode))
}
// At this point we've made a successful request (be it from a retry or
// otherwise) and can read and process the response body.
body, err := io.ReadAll(&io.LimitedReader{R: httpResponse.Body, N: maxResponseSize})
closeErr := httpResponse.Body.Close()
if err == nil {
err = closeErr
}
if err != nil {
return nil, records, newIPError(target, berrors.UnauthorizedError("Error reading HTTP response body: %v", err))
}
// io.LimitedReader will silently truncate a Reader so if the
// resulting payload is the same size as maxResponseSize fail
if len(body) >= maxResponseSize {
return nil, records, newIPError(target, berrors.UnauthorizedError("Invalid response from %s: %q",
records[len(records)-1].URL, body))
}
// We were successful, so record the negotiated key exchange mechanism in the
// last validationRecord.
// TODO(#7321): Remove this when we have collected enough data.
if httpResponse.TLS != nil {
records[len(records)-1].UsedRSAKEX = usedRSAKEX(httpResponse.TLS.CipherSuite)
}
return body, records, nil
}
func (va *ValidationAuthorityImpl) validateHTTP01(ctx context.Context, ident identifier.ACMEIdentifier, challenge core.Challenge) ([]core.ValidationRecord, error) {
if ident.Type != identifier.DNS {
va.log.Infof("Got non-DNS identifier for HTTP validation: %s", ident)
return nil, berrors.MalformedError("Identifier type for HTTP validation was not DNS")
}
// Perform the fetch
path := fmt.Sprintf(".well-known/acme-challenge/%s", challenge.Token)
body, validationRecords, err := va.fetchHTTP(ctx, ident.Value, "/"+path)
if err != nil {
return validationRecords, err
}
payload := strings.TrimRightFunc(string(body), unicode.IsSpace)
if payload != challenge.ProvidedKeyAuthorization {
problem := berrors.UnauthorizedError("The key authorization file from the server did not match this challenge. Expected %q (got %q)",
challenge.ProvidedKeyAuthorization, payload)
va.log.Infof("%s for %s", problem, ident)
return validationRecords, problem
}
return validationRecords, nil
}