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:
Daniel McCarney 2019-01-21 15:08:40 -05:00 committed by GitHub
parent 1a68cc2225
commit 98663717d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 257 additions and 401 deletions

View File

@ -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']))

View File

@ -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 {

View File

@ -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"),
},