VA: Rework SimplifiedVAHTTP for pre-resolved dials. (#4016)
The URL construction approach we were previously using for the refactored VA HTTP-01 validation code was nice but broke SNI for HTTP->HTTPS redirects. In order to preserve this functionality we need to use a custom `DialContext` handler on the HTTP Transport that overrides the target host to use a pre-resolved IP. Resolves https://github.com/letsencrypt/boulder/issues/3969
This commit is contained in:
parent
1a68cc2225
commit
98663717d8
|
|
@ -303,10 +303,6 @@ def test_http_challenge_https_redirect():
|
|||
for r in redirectedRequests:
|
||||
if r['HTTPS'] is False:
|
||||
raise Exception("Expected all redirected requests to be HTTPS")
|
||||
# TODO(@cpu): The following ServerName test will fail with config-next
|
||||
# until https://github.com/letsencrypt/boulder/issues/3969 is fixed.
|
||||
if default_config_dir.startswith("test/config-next"):
|
||||
return
|
||||
elif r['ServerName'] != d:
|
||||
raise Exception("Expected all redirected requests to have ServerName {0} got \"{1}\"".format(d, r['ServerName']))
|
||||
|
||||
|
|
|
|||
294
va/http.go
294
va/http.go
|
|
@ -17,13 +17,50 @@ import (
|
|||
"github.com/letsencrypt/boulder/probs"
|
||||
)
|
||||
|
||||
// shavedDialContext shaves 10ms off of the context it was given before
|
||||
// calling the default DialContext. This helps us be able to differentiate
|
||||
// between timeouts during connect and timeouts after connect.
|
||||
func shavedDialContext(
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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,
|
||||
addr string) (net.Conn, error) {
|
||||
origAddr string) (net.Conn, error) {
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
// Shouldn't happen: All requests should have a deadline by this point.
|
||||
|
|
@ -37,74 +74,64 @@ func shavedDialContext(
|
|||
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))
|
||||
|
||||
// Invoke the default transport's original DialContext function using the
|
||||
// reconstructed context.
|
||||
defaultTransport, ok := http.DefaultTransport.(*http.Transport)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("DefaultTransport was not an http.Transport")
|
||||
}
|
||||
return defaultTransport.DialContext(ctx, network, addr)
|
||||
return defaultTransport.DialContext(ctx, network, targetAddr)
|
||||
}
|
||||
|
||||
// redirectChecker is a function that can be used for an HTTP Client's
|
||||
// checkRedirect function.
|
||||
type redirectChecker func(*http.Request, []*http.Request) error
|
||||
// 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)
|
||||
|
||||
// newHTTPClient constructs a HTTP client with a custom transport suitable for
|
||||
// HTTP-01 validation. The provided checkRedirect function is used as the
|
||||
// client's checkRedirect handler.
|
||||
func newHTTPClient(checkRedirect redirectChecker) http.Client {
|
||||
// Construct a one-off HTTP client with a custom transport.
|
||||
return http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: shavedDialContext,
|
||||
// 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,
|
||||
},
|
||||
CheckRedirect: checkRedirect,
|
||||
}
|
||||
}
|
||||
|
||||
// httpValidationURL constructs a URL for the given IP address, path and port
|
||||
// combination. The port is omitted from the URL if it is the default HTTP
|
||||
// port or the default HTTPS port. The protocol scheme of the URL is HTTP unless
|
||||
// useHTTPS is true. UseHTTPS should only be true when constructing validation
|
||||
// URLs based on a redirect from an initial HTTP validation request.
|
||||
func httpValidationURL(validationIP net.IP, port int, path, query string, useHTTPS bool) *url.URL {
|
||||
urlHost := validationIP.String()
|
||||
|
||||
// If the port is something other than the conventional HTTP or HTTPS port,
|
||||
// put it in the URL explicitly using `net.JoinHostPort`.
|
||||
if port != 80 && port != 443 {
|
||||
urlHost = net.JoinHostPort(validationIP.String(), strconv.Itoa(port))
|
||||
}
|
||||
|
||||
// if the validation IP is an IPv6 address, and we aren't using
|
||||
// `net.JoinHostPort` then we have to manually surround the IPv6 address
|
||||
// with square brackets to make a valid IPv6 URL (e.g "http://[::1]/foo" not
|
||||
// "http://::1/foo")
|
||||
if (port == 80 || port == 443) && validationIP.To4() == nil {
|
||||
urlHost = fmt.Sprintf("[%s]", urlHost)
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if useHTTPS {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
return &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: urlHost,
|
||||
Path: path,
|
||||
RawQuery: query,
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -267,16 +294,19 @@ func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (stri
|
|||
return reqHost, reqPort, nil
|
||||
}
|
||||
|
||||
// setupHTTPValidation can be used in two ways:
|
||||
// 1) To create and setup the initial validation request for a target by
|
||||
// providing a nil req.
|
||||
// 2) To mutate an existing HTTP request to use a URL/Host based on resolved IP
|
||||
// addresses.
|
||||
// The second is helpful when processing redirect requests.
|
||||
// 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(
|
||||
ctx context.Context,
|
||||
req *http.Request,
|
||||
target *httpValidationTarget) (*http.Request, core.ValidationRecord, error) {
|
||||
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.
|
||||
|
|
@ -285,63 +315,32 @@ func (va *ValidationAuthorityImpl) setupHTTPValidation(
|
|||
fmt.Errorf("httpValidationTarget can not be nil")
|
||||
}
|
||||
|
||||
// Construct a base validation record with the target's information.
|
||||
// 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,
|
||||
}
|
||||
|
||||
// Build a URL with the target's IP address and port
|
||||
// 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)
|
||||
return nil,
|
||||
record,
|
||||
fmt.Errorf(
|
||||
"host %q has no IP addresses remaining to use",
|
||||
target.host)
|
||||
}
|
||||
|
||||
var useHTTPS bool
|
||||
// If we are mutating an existing redirected request and the original request
|
||||
// URL uses HTTPS then we must construct a validation URL using HTTPS. In all
|
||||
// other cases we construct an HTTP URL.
|
||||
if req != nil && req.URL.Scheme == "https" {
|
||||
useHTTPS = true
|
||||
}
|
||||
|
||||
record.AddressUsed = targetIP
|
||||
url := httpValidationURL(targetIP, target.port, target.path, target.query, useHTTPS)
|
||||
record.URL = url.String()
|
||||
|
||||
// If there's no provided HTTP request to mutate (e.g. a redirect request
|
||||
// we're following as part of a validation) then construct a new initial HTTP
|
||||
// GET request for the validation.
|
||||
if req == nil {
|
||||
var err error
|
||||
req, err = http.NewRequest("GET", url.String(), nil)
|
||||
if err != nil {
|
||||
return nil, record, err
|
||||
}
|
||||
// Immediately reconstruct the request using the validation context
|
||||
req = req.WithContext(ctx)
|
||||
if va.userAgent != "" {
|
||||
req.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.
|
||||
req.Header.Set("Accept", "*/*")
|
||||
dialer := &preresolvedDialer{
|
||||
ip: targetIP,
|
||||
port: target.port,
|
||||
hostname: target.host,
|
||||
}
|
||||
|
||||
// Override the request's target URL and Host
|
||||
req.URL = url
|
||||
req.Host = target.host
|
||||
return req, record, nil
|
||||
return dialer, record, nil
|
||||
}
|
||||
|
||||
// fetchHTTPSimple invokes processHTTPValidation and if an error result is
|
||||
|
|
@ -397,12 +396,42 @@ func (va *ValidationAuthorityImpl) processHTTPValidation(
|
|||
return nil, nil, 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, err
|
||||
}
|
||||
// Immediately reconstruct the request using the validation context
|
||||
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
|
||||
initialReq, baseRecord, err := va.setupHTTPValidation(ctx, nil, target)
|
||||
dialer, baseRecord, err := va.setupHTTPValidation(ctx, initialReq.URL.String(), target)
|
||||
if err != nil {
|
||||
return nil, []core.ValidationRecord{}, 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())
|
||||
|
||||
|
|
@ -435,28 +464,34 @@ func (va *ValidationAuthorityImpl) processHTTPValidation(
|
|||
redirQuery = req.URL.RawQuery
|
||||
}
|
||||
|
||||
// Setup a validation target for the redirect host. This will resolve IP
|
||||
// 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
|
||||
}
|
||||
|
||||
// Mutate the existing redirect request to use a URL and Host based on the
|
||||
// explicitly resolved target IPs. This will also give us a validationRecord
|
||||
// for the redirect which we should append to the records.
|
||||
_, redirRecord, err := va.setupHTTPValidation(ctx, req, redirTarget)
|
||||
// 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(ctx, req.URL.String(), redirTarget)
|
||||
records = append(records, redirRecord)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
va.log.Debugf("following redirect to host %q url %q\n", 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 and check HTTP redirects it encounters with
|
||||
// processRedirect
|
||||
client := newHTTPClient(processRedirect)
|
||||
// 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.
|
||||
|
|
@ -472,15 +507,18 @@ func (va *ValidationAuthorityImpl) processHTTPValidation(
|
|||
|
||||
// setup another validation to retry the target with the new IP and append
|
||||
// the retry record.
|
||||
retryReq, retryRecord, err := va.setupHTTPValidation(ctx, nil, target)
|
||||
retryDialer, retryRecord, err := va.setupHTTPValidation(ctx, initialReq.URL.String(), target)
|
||||
records = append(records, retryRecord)
|
||||
if err != nil {
|
||||
return nil, records, 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(retryReq)
|
||||
httpResponse, err = client.Do(initialReq)
|
||||
// If the retry still failed there isn't anything more to do, return the
|
||||
// error immediately.
|
||||
if err != nil {
|
||||
|
|
|
|||
360
va/http_test.go
360
va/http_test.go
|
|
@ -20,183 +20,42 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestNewHTTPClient(t *testing.T) {
|
||||
dummyRedirHandler := func(_ *http.Request, _ []*http.Request) error {
|
||||
return nil
|
||||
// TestDialerMismatchError tests that using a preresolvedDialer for one host for
|
||||
// a dial to another host produces the expected dialerMismatchError.
|
||||
func TestDialerMismatchError(t *testing.T) {
|
||||
d := preresolvedDialer{
|
||||
ip: net.ParseIP("127.0.0.1"),
|
||||
port: 1337,
|
||||
hostname: "letsencrypt.org",
|
||||
}
|
||||
client := newHTTPClient(dummyRedirHandler)
|
||||
|
||||
// The client should have a HTTP Transport
|
||||
rawTransport := client.Transport
|
||||
if httpTrans, ok := rawTransport.(*http.Transport); !ok {
|
||||
t.Fatalf(
|
||||
"newHTTPClient returned a client with a Transport of the wrong type: "+
|
||||
"%t not http.Transport",
|
||||
rawTransport)
|
||||
} else {
|
||||
// The HTTP Transport should have a TLS config that skips verifying
|
||||
// certificates.
|
||||
test.AssertEquals(t, httpTrans.TLSClientConfig.InsecureSkipVerify, true)
|
||||
// Keep alives should be disabled
|
||||
test.AssertEquals(t, httpTrans.DisableKeepAlives, true)
|
||||
test.AssertEquals(t, httpTrans.MaxIdleConns, 1)
|
||||
test.AssertEquals(t, httpTrans.IdleConnTimeout.String(), "1s")
|
||||
test.AssertEquals(t, httpTrans.TLSHandshakeTimeout.String(), "10s")
|
||||
expectedErr := dialerMismatchError{
|
||||
dialerHost: d.hostname,
|
||||
dialerIP: d.ip.String(),
|
||||
dialerPort: d.port,
|
||||
host: "lettuceencrypt.org",
|
||||
}
|
||||
|
||||
_, err := d.DialContext(
|
||||
context.Background(),
|
||||
"tincan-and-string",
|
||||
"lettuceencrypt.org:80")
|
||||
test.AssertEquals(t, err.Error(), expectedErr.Error())
|
||||
}
|
||||
|
||||
func TestHTTPValidationURL(t *testing.T) {
|
||||
egPath := "/.well-known/.less-known/.obscure"
|
||||
egQuery := "true=false&up=down&left=right"
|
||||
testCases := []struct {
|
||||
Name string
|
||||
IP string
|
||||
Port int
|
||||
Path string
|
||||
Query string
|
||||
UseHTTPS bool
|
||||
ExpectedURL string
|
||||
}{
|
||||
{
|
||||
Name: "IPv4 Standard HTTP port",
|
||||
IP: "10.10.10.10",
|
||||
Port: 80,
|
||||
Path: egPath,
|
||||
ExpectedURL: fmt.Sprintf("http://10.10.10.10%s", egPath),
|
||||
},
|
||||
{
|
||||
Name: "IPv4 Standard HTTP port with query",
|
||||
IP: "10.10.10.10",
|
||||
Port: 80,
|
||||
Path: egPath,
|
||||
Query: egQuery,
|
||||
ExpectedURL: fmt.Sprintf("http://10.10.10.10%s?%s", egPath, egQuery),
|
||||
},
|
||||
{
|
||||
Name: "IPv4 Non-standard HTTP port",
|
||||
IP: "15.15.15.15",
|
||||
Path: egPath,
|
||||
Port: 8080,
|
||||
ExpectedURL: fmt.Sprintf("http://15.15.15.15:8080%s", egPath),
|
||||
},
|
||||
{
|
||||
Name: "IPv4 Non-standard HTTP port with query",
|
||||
IP: "15.15.15.15",
|
||||
Port: 8080,
|
||||
Path: egPath,
|
||||
Query: egQuery,
|
||||
ExpectedURL: fmt.Sprintf("http://15.15.15.15:8080%s?%s", egPath, egQuery),
|
||||
},
|
||||
{
|
||||
Name: "IPv6 Standard HTTP port",
|
||||
IP: "::1",
|
||||
Port: 80,
|
||||
Path: egPath,
|
||||
ExpectedURL: fmt.Sprintf("http://[::1]%s", egPath),
|
||||
},
|
||||
{
|
||||
Name: "IPv6 Standard HTTP port with query",
|
||||
IP: "::1",
|
||||
Port: 80,
|
||||
Path: egPath,
|
||||
Query: egQuery,
|
||||
ExpectedURL: fmt.Sprintf("http://[::1]%s?%s", egPath, egQuery),
|
||||
},
|
||||
{
|
||||
Name: "IPv6 Non-standard HTTP port",
|
||||
IP: "::1",
|
||||
Port: 8080,
|
||||
Path: egPath,
|
||||
ExpectedURL: fmt.Sprintf("http://[::1]:8080%s", egPath),
|
||||
},
|
||||
{
|
||||
Name: "IPv6 Non-standard HTTP port with query",
|
||||
IP: "::1",
|
||||
Port: 8080,
|
||||
Path: egPath,
|
||||
Query: egQuery,
|
||||
ExpectedURL: fmt.Sprintf("http://[::1]:8080%s?%s", egPath, egQuery),
|
||||
},
|
||||
{
|
||||
Name: "IPv4 Standard HTTPS port",
|
||||
IP: "10.10.10.10",
|
||||
Port: 443,
|
||||
Path: egPath,
|
||||
UseHTTPS: true,
|
||||
ExpectedURL: fmt.Sprintf("https://10.10.10.10%s", egPath),
|
||||
},
|
||||
{
|
||||
Name: "IPv4 Standard HTTPS port with query",
|
||||
IP: "10.10.10.10",
|
||||
Port: 443,
|
||||
Path: egPath,
|
||||
Query: egQuery,
|
||||
UseHTTPS: true,
|
||||
ExpectedURL: fmt.Sprintf("https://10.10.10.10%s?%s", egPath, egQuery),
|
||||
},
|
||||
{
|
||||
Name: "IPv4 Non-standard HTTPS port",
|
||||
IP: "15.15.15.15",
|
||||
Port: 4443,
|
||||
Path: egPath,
|
||||
UseHTTPS: true,
|
||||
ExpectedURL: fmt.Sprintf("https://15.15.15.15:4443%s", egPath),
|
||||
},
|
||||
{
|
||||
Name: "IPv4 Non-standard HTTPS port with query",
|
||||
IP: "15.15.15.15",
|
||||
Port: 4443,
|
||||
Path: egPath,
|
||||
Query: egQuery,
|
||||
UseHTTPS: true,
|
||||
ExpectedURL: fmt.Sprintf("https://15.15.15.15:4443%s?%s", egPath, egQuery),
|
||||
},
|
||||
{
|
||||
Name: "IPv6 Standard HTTPS port",
|
||||
IP: "::1",
|
||||
Port: 443,
|
||||
Path: egPath,
|
||||
UseHTTPS: true,
|
||||
ExpectedURL: fmt.Sprintf("https://[::1]%s", egPath),
|
||||
},
|
||||
{
|
||||
Name: "IPv6 Standard HTTPS port with query",
|
||||
IP: "::1",
|
||||
Port: 443,
|
||||
Path: egPath,
|
||||
Query: egQuery,
|
||||
UseHTTPS: true,
|
||||
ExpectedURL: fmt.Sprintf("https://[::1]%s?%s", egPath, egQuery),
|
||||
},
|
||||
{
|
||||
Name: "IPv6 Non-standard HTTPS port",
|
||||
IP: "::1",
|
||||
Port: 4443,
|
||||
Path: egPath,
|
||||
UseHTTPS: true,
|
||||
ExpectedURL: fmt.Sprintf("https://[::1]:4443%s", egPath),
|
||||
},
|
||||
{
|
||||
Name: "IPv6 Non-standard HTTPS port with query",
|
||||
IP: "::1",
|
||||
Port: 4443,
|
||||
Path: egPath,
|
||||
Query: egQuery,
|
||||
UseHTTPS: true,
|
||||
ExpectedURL: fmt.Sprintf("https://[::1]:4443%s?%s", egPath, egQuery),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
ipAddr := net.ParseIP(tc.IP)
|
||||
if ipAddr == nil {
|
||||
t.Fatalf("Failed to parse test case %q IP %q", tc.Name, tc.IP)
|
||||
}
|
||||
url := httpValidationURL(ipAddr, tc.Port, tc.Path, tc.Query, tc.UseHTTPS)
|
||||
test.AssertEquals(t, url.String(), tc.ExpectedURL)
|
||||
})
|
||||
func TestHTTPTransport(t *testing.T) {
|
||||
dummyDialerFunc := func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||
return nil, nil
|
||||
}
|
||||
transport := httpTransport(dummyDialerFunc)
|
||||
// The HTTP Transport should have a TLS config that skips verifying
|
||||
// certificates.
|
||||
test.AssertEquals(t, transport.TLSClientConfig.InsecureSkipVerify, true)
|
||||
// Keep alives should be disabled
|
||||
test.AssertEquals(t, transport.DisableKeepAlives, true)
|
||||
test.AssertEquals(t, transport.MaxIdleConns, 1)
|
||||
test.AssertEquals(t, transport.IdleConnTimeout.String(), "1s")
|
||||
test.AssertEquals(t, transport.TLSHandshakeTimeout.String(), "10s")
|
||||
}
|
||||
|
||||
func TestHTTPValidationTarget(t *testing.T) {
|
||||
|
|
@ -387,111 +246,81 @@ func TestSetupHTTPValidation(t *testing.T) {
|
|||
return target
|
||||
}
|
||||
|
||||
httpInputURL, err := url.Parse("http://ipv4.and.ipv6.localhost/yellow/brick/road")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct test httpInputURL")
|
||||
}
|
||||
httpsInputURL, err := url.Parse("https://ipv4.and.ipv6.localhost/yellow/brick/road")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct test httpsInputURL")
|
||||
}
|
||||
httpInputURL := "http://ipv4.and.ipv6.localhost/yellow/brick/road"
|
||||
httpsInputURL := "https://ipv4.and.ipv6.localhost/yellow/brick/road"
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
InputReq *http.Request
|
||||
InputTarget *httpValidationTarget
|
||||
ExpectedRequestHost string
|
||||
ExpectedRequestURL string
|
||||
ExpectedRecord core.ValidationRecord
|
||||
ExpectedError error
|
||||
Name string
|
||||
InputURL string
|
||||
InputTarget *httpValidationTarget
|
||||
ExpectedRecord core.ValidationRecord
|
||||
ExpectedDialer *preresolvedDialer
|
||||
ExpectedError error
|
||||
}{
|
||||
{
|
||||
Name: "nil target",
|
||||
InputURL: httpInputURL,
|
||||
ExpectedError: fmt.Errorf("httpValidationTarget can not be nil"),
|
||||
},
|
||||
{
|
||||
Name: "target with no IPs",
|
||||
Name: "empty input URL",
|
||||
InputTarget: &httpValidationTarget{},
|
||||
ExpectedError: fmt.Errorf("reqURL can not be nil"),
|
||||
},
|
||||
{
|
||||
Name: "target with no IPs",
|
||||
InputURL: httpInputURL,
|
||||
InputTarget: &httpValidationTarget{
|
||||
host: "foobar",
|
||||
port: va.httpPort,
|
||||
path: "idk",
|
||||
},
|
||||
// With a broken target no URL is added to the validation record because
|
||||
// there was no IP to construct it with.
|
||||
ExpectedRecord: core.ValidationRecord{
|
||||
URL: "http://ipv4.and.ipv6.localhost/yellow/brick/road",
|
||||
Hostname: "foobar",
|
||||
Port: strconv.Itoa(va.httpPort),
|
||||
},
|
||||
ExpectedError: fmt.Errorf(`host "foobar" has no IP addresses remaining to use`),
|
||||
},
|
||||
{
|
||||
Name: "nil input req",
|
||||
InputTarget: mustTarget(t, "example.com", 9999, "/.well-known/stuff"),
|
||||
ExpectedRequestHost: "example.com",
|
||||
ExpectedRequestURL: "http://127.0.0.1:9999/.well-known/stuff",
|
||||
ExpectedRecord: core.ValidationRecord{
|
||||
Hostname: "example.com",
|
||||
Port: "9999",
|
||||
URL: "http://127.0.0.1:9999/.well-known/stuff",
|
||||
AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
AddressUsed: net.ParseIP("127.0.0.1"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "non-nil non-standard port input req",
|
||||
InputTarget: mustTarget(t, "ipv4.and.ipv6.localhost", 808, "/yellow/brick/road"),
|
||||
InputReq: &http.Request{
|
||||
URL: httpInputURL,
|
||||
},
|
||||
ExpectedRequestHost: "ipv4.and.ipv6.localhost",
|
||||
ExpectedRequestURL: "http://[::1]:808/yellow/brick/road",
|
||||
ExpectedRecord: core.ValidationRecord{
|
||||
Hostname: "ipv4.and.ipv6.localhost",
|
||||
Port: "808",
|
||||
URL: "http://[::1]:808/yellow/brick/road",
|
||||
AddressesResolved: []net.IP{net.ParseIP("::1"), net.ParseIP("127.0.0.1")},
|
||||
AddressUsed: net.ParseIP("::1"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "non-nil HTTP input req",
|
||||
Name: "HTTP input req",
|
||||
InputTarget: mustTarget(t, "ipv4.and.ipv6.localhost", va.httpPort, "/yellow/brick/road"),
|
||||
InputReq: &http.Request{
|
||||
URL: httpInputURL,
|
||||
},
|
||||
ExpectedRequestHost: "ipv4.and.ipv6.localhost",
|
||||
ExpectedRequestURL: "http://[::1]/yellow/brick/road",
|
||||
InputURL: httpInputURL,
|
||||
ExpectedRecord: core.ValidationRecord{
|
||||
Hostname: "ipv4.and.ipv6.localhost",
|
||||
Port: strconv.Itoa(va.httpPort),
|
||||
URL: "http://[::1]/yellow/brick/road",
|
||||
URL: "http://ipv4.and.ipv6.localhost/yellow/brick/road",
|
||||
AddressesResolved: []net.IP{net.ParseIP("::1"), net.ParseIP("127.0.0.1")},
|
||||
AddressUsed: net.ParseIP("::1"),
|
||||
},
|
||||
ExpectedDialer: &preresolvedDialer{
|
||||
ip: net.ParseIP("::1"),
|
||||
port: va.httpPort,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "non-nil HTTPS input req",
|
||||
Name: "HTTPS input req",
|
||||
InputTarget: mustTarget(t, "ipv4.and.ipv6.localhost", va.httpsPort, "/yellow/brick/road"),
|
||||
InputReq: &http.Request{
|
||||
URL: httpsInputURL,
|
||||
},
|
||||
ExpectedRequestHost: "ipv4.and.ipv6.localhost",
|
||||
ExpectedRequestURL: "https://[::1]/yellow/brick/road",
|
||||
InputURL: httpsInputURL,
|
||||
ExpectedRecord: core.ValidationRecord{
|
||||
Hostname: "ipv4.and.ipv6.localhost",
|
||||
Port: strconv.Itoa(va.httpsPort),
|
||||
URL: "https://[::1]/yellow/brick/road",
|
||||
URL: "https://ipv4.and.ipv6.localhost/yellow/brick/road",
|
||||
AddressesResolved: []net.IP{net.ParseIP("::1"), net.ParseIP("127.0.0.1")},
|
||||
AddressUsed: net.ParseIP("::1"),
|
||||
},
|
||||
ExpectedDialer: &preresolvedDialer{
|
||||
ip: net.ParseIP("::1"),
|
||||
port: va.httpsPort,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
outReq, outRecord, err := va.setupHTTPValidation(
|
||||
outDialer, outRecord, err := va.setupHTTPValidation(
|
||||
context.Background(),
|
||||
tc.InputReq,
|
||||
tc.InputURL,
|
||||
tc.InputTarget)
|
||||
|
||||
if err != nil && tc.ExpectedError == nil {
|
||||
|
|
@ -500,23 +329,14 @@ func TestSetupHTTPValidation(t *testing.T) {
|
|||
t.Errorf("Expected %v error, got nil", tc.ExpectedError)
|
||||
} else if err != nil && tc.ExpectedError != nil {
|
||||
test.AssertEquals(t, err.Error(), tc.ExpectedError.Error())
|
||||
} else {
|
||||
test.AssertEquals(t, outReq.Host, tc.ExpectedRequestHost)
|
||||
test.AssertEquals(t, outReq.URL.String(), tc.ExpectedRequestURL)
|
||||
}
|
||||
if tc.ExpectedDialer == nil && outDialer != nil {
|
||||
t.Errorf("Expected nil dialer, got %v", outDialer)
|
||||
} else if tc.ExpectedDialer != nil {
|
||||
test.AssertMarshaledEquals(t, outDialer, tc.ExpectedDialer)
|
||||
}
|
||||
// In all cases we expect there to have been a validation record
|
||||
test.AssertMarshaledEquals(t, outRecord, tc.ExpectedRecord)
|
||||
// If the input request was nil then check that the constructed outReq has
|
||||
// the right UA and Accept header values.
|
||||
if tc.InputReq == nil && err == nil {
|
||||
test.AssertEquals(t, outReq.Header.Get("User-Agent"), va.userAgent)
|
||||
test.AssertEquals(t, outReq.Header.Get("Accept"), "*/*")
|
||||
} else if tc.InputReq != nil && err == nil {
|
||||
// Otherwise if there was an input req make sure its URL and Host were
|
||||
// mutated as expected.
|
||||
test.AssertEquals(t, tc.InputReq.Host, tc.ExpectedRequestHost)
|
||||
test.AssertEquals(t, tc.InputReq.URL.String(), tc.ExpectedRequestURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -727,11 +547,16 @@ func TestFetchHTTPSimple(t *testing.T) {
|
|||
// giving a termination criteria of > maxRedirect+1
|
||||
expectedLoopRecords := []core.ValidationRecord{}
|
||||
for i := 0; i <= maxRedirect+1; i++ {
|
||||
// The first request will not have a port # in the URL.
|
||||
url := "http://example.com/loop"
|
||||
if i != 0 {
|
||||
url = fmt.Sprintf("http://example.com:%d/loop", httpPort)
|
||||
}
|
||||
expectedLoopRecords = append(expectedLoopRecords,
|
||||
core.ValidationRecord{
|
||||
Hostname: "example.com",
|
||||
Port: strconv.Itoa(httpPort),
|
||||
URL: fmt.Sprintf("http://127.0.0.1:%d/loop", httpPort),
|
||||
URL: url,
|
||||
AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
AddressUsed: net.ParseIP("127.0.0.1"),
|
||||
})
|
||||
|
|
@ -765,13 +590,13 @@ func TestFetchHTTPSimple(t *testing.T) {
|
|||
Host: "example.com",
|
||||
Path: "/timeout",
|
||||
ExpectedProblem: probs.ConnectionFailure(
|
||||
"Fetching http://127.0.0.1:%d/timeout: "+
|
||||
"Timeout after connect (your server may be slow or overloaded)", httpPort),
|
||||
"Fetching http://example.com/timeout: " +
|
||||
"Timeout after connect (your server may be slow or overloaded)"),
|
||||
ExpectedRecords: []core.ValidationRecord{
|
||||
core.ValidationRecord{
|
||||
Hostname: "example.com",
|
||||
Port: strconv.Itoa(httpPort),
|
||||
URL: fmt.Sprintf("http://127.0.0.1:%d/timeout", httpPort),
|
||||
URL: "http://example.com/timeout",
|
||||
AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
AddressUsed: net.ParseIP("127.0.0.1"),
|
||||
},
|
||||
|
|
@ -797,7 +622,7 @@ func TestFetchHTTPSimple(t *testing.T) {
|
|||
core.ValidationRecord{
|
||||
Hostname: "example.com",
|
||||
Port: strconv.Itoa(httpPort),
|
||||
URL: fmt.Sprintf("http://127.0.0.1:%d/redir-bad-proto", httpPort),
|
||||
URL: "http://example.com/redir-bad-proto",
|
||||
AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
AddressUsed: net.ParseIP("127.0.0.1"),
|
||||
},
|
||||
|
|
@ -814,7 +639,7 @@ func TestFetchHTTPSimple(t *testing.T) {
|
|||
core.ValidationRecord{
|
||||
Hostname: "example.com",
|
||||
Port: strconv.Itoa(httpPort),
|
||||
URL: fmt.Sprintf("http://127.0.0.1:%d/redir-bad-port", httpPort),
|
||||
URL: "http://example.com/redir-bad-port",
|
||||
AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
AddressUsed: net.ParseIP("127.0.0.1"),
|
||||
},
|
||||
|
|
@ -831,7 +656,7 @@ func TestFetchHTTPSimple(t *testing.T) {
|
|||
core.ValidationRecord{
|
||||
Hostname: "example.com",
|
||||
Port: strconv.Itoa(httpPort),
|
||||
URL: fmt.Sprintf("http://127.0.0.1:%d/redir-bad-host", httpPort),
|
||||
URL: "http://example.com/redir-bad-host",
|
||||
AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
AddressUsed: net.ParseIP("127.0.0.1"),
|
||||
},
|
||||
|
|
@ -842,15 +667,13 @@ func TestFetchHTTPSimple(t *testing.T) {
|
|||
Host: "example.com",
|
||||
Path: "/bad-status-code",
|
||||
ExpectedProblem: probs.Unauthorized(
|
||||
"Invalid response from http://127.0.0.1:%d/bad-status-code "+
|
||||
"[127.0.0.1]: 410",
|
||||
httpPort,
|
||||
),
|
||||
"Invalid response from http://example.com/bad-status-code " +
|
||||
"[127.0.0.1]: 410"),
|
||||
ExpectedRecords: []core.ValidationRecord{
|
||||
core.ValidationRecord{
|
||||
Hostname: "example.com",
|
||||
Port: strconv.Itoa(httpPort),
|
||||
URL: fmt.Sprintf("http://127.0.0.1:%d/bad-status-code", httpPort),
|
||||
URL: "http://example.com/bad-status-code",
|
||||
AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
AddressUsed: net.ParseIP("127.0.0.1"),
|
||||
},
|
||||
|
|
@ -861,14 +684,14 @@ func TestFetchHTTPSimple(t *testing.T) {
|
|||
Host: "example.com",
|
||||
Path: "/resp-too-big",
|
||||
ExpectedProblem: probs.Unauthorized(
|
||||
"Invalid response from http://127.0.0.1:%d/resp-too-big "+
|
||||
"[127.0.0.1]: %q", httpPort, expectedTruncatedResp.String(),
|
||||
"Invalid response from http://example.com/resp-too-big "+
|
||||
"[127.0.0.1]: %q", expectedTruncatedResp.String(),
|
||||
),
|
||||
ExpectedRecords: []core.ValidationRecord{
|
||||
core.ValidationRecord{
|
||||
Hostname: "example.com",
|
||||
Port: strconv.Itoa(httpPort),
|
||||
URL: fmt.Sprintf("http://127.0.0.1:%d/resp-too-big", httpPort),
|
||||
URL: "http://example.com/resp-too-big",
|
||||
AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
AddressUsed: net.ParseIP("127.0.0.1"),
|
||||
},
|
||||
|
|
@ -879,13 +702,12 @@ func TestFetchHTTPSimple(t *testing.T) {
|
|||
Host: "ipv6.localhost",
|
||||
Path: "/ok",
|
||||
ExpectedProblem: probs.ConnectionFailure(
|
||||
"Fetching http://[::1]:%d/ok: Error getting validation data", httpPort,
|
||||
),
|
||||
"Fetching http://ipv6.localhost/ok: Error getting validation data"),
|
||||
ExpectedRecords: []core.ValidationRecord{
|
||||
core.ValidationRecord{
|
||||
Hostname: "ipv6.localhost",
|
||||
Port: strconv.Itoa(httpPort),
|
||||
URL: fmt.Sprintf("http://[::1]:%d/ok", httpPort),
|
||||
URL: "http://ipv6.localhost/ok",
|
||||
AddressesResolved: []net.IP{net.ParseIP("::1")},
|
||||
AddressUsed: net.ParseIP("::1"),
|
||||
},
|
||||
|
|
@ -900,7 +722,7 @@ func TestFetchHTTPSimple(t *testing.T) {
|
|||
core.ValidationRecord{
|
||||
Hostname: "ipv4.and.ipv6.localhost",
|
||||
Port: strconv.Itoa(httpPort),
|
||||
URL: fmt.Sprintf("http://[::1]:%d/ok", httpPort),
|
||||
URL: "http://ipv4.and.ipv6.localhost/ok",
|
||||
AddressesResolved: []net.IP{net.ParseIP("::1"), net.ParseIP("127.0.0.1")},
|
||||
// The first validation record should have used the IPv6 addr
|
||||
AddressUsed: net.ParseIP("::1"),
|
||||
|
|
@ -908,7 +730,7 @@ func TestFetchHTTPSimple(t *testing.T) {
|
|||
core.ValidationRecord{
|
||||
Hostname: "ipv4.and.ipv6.localhost",
|
||||
Port: strconv.Itoa(httpPort),
|
||||
URL: fmt.Sprintf("http://127.0.0.1:%d/ok", httpPort),
|
||||
URL: "http://ipv4.and.ipv6.localhost/ok",
|
||||
AddressesResolved: []net.IP{net.ParseIP("::1"), net.ParseIP("127.0.0.1")},
|
||||
// The second validation record should have used the IPv4 addr as a fallback
|
||||
AddressUsed: net.ParseIP("127.0.0.1"),
|
||||
|
|
@ -924,7 +746,7 @@ func TestFetchHTTPSimple(t *testing.T) {
|
|||
core.ValidationRecord{
|
||||
Hostname: "example.com",
|
||||
Port: strconv.Itoa(httpPort),
|
||||
URL: fmt.Sprintf("http://127.0.0.1:%d/ok", httpPort),
|
||||
URL: "http://example.com/ok",
|
||||
AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
AddressUsed: net.ParseIP("127.0.0.1"),
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue