...

Source file src/github.com/letsencrypt/boulder/va/http.go

Documentation: github.com/letsencrypt/boulder/va

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

View as plain text