VA: Add integration test for HTTP timeouts. (#4050)

Also update `TestHTTPTimeout` to test with the `SimplifiedVAHTTP`
feature flag enabled.
This commit is contained in:
Daniel McCarney 2019-02-12 16:42:01 -05:00 committed by Jacob Hoffman-Andrews
parent c37355b40b
commit 1c0be52e53
6 changed files with 113 additions and 28 deletions

View File

@ -4,7 +4,7 @@ services:
# To minimize fetching this should be the same version used below
image: letsencrypt/boulder-tools-go${TRAVIS_GO_VERSION:-1.11.5}:2019-02-11
environment:
FAKE_DNS: 127.0.0.1
FAKE_DNS: 10.77.77.77
PKCS11_PROXY_SOCKET: tcp://boulder-hsm:5657
BOULDER_CONFIG_DIR: test/config
volumes:

View File

@ -18,7 +18,7 @@
},
"vaService": {
"serverAddress": "va.boulder:9092",
"timeout": "20s"
"timeout": "2s"
},
"caService": {
"serverAddress": "ca.boulder:9093",

View File

@ -23,7 +23,7 @@
},
"vaService": {
"serverAddress": "va.boulder:9092",
"timeout": "20s"
"timeout": "2s"
},
"caService": {
"serverAddress": "ca.boulder:9093",

View File

@ -306,6 +306,65 @@ def test_http_challenge_https_redirect():
elif r['ServerName'] != d:
raise Exception("Expected all redirected requests to have ServerName {0} got \"{1}\"".format(d, r['ServerName']))
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
class SlowHTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
try:
# Sleeptime needs to be larger than the RA->VA timeout (2s at the
# time of writing)
sleeptime = 5
print("SlowHTTPRequestHandler: sleeping for {0}s\n".format(sleeptime))
time.sleep(sleeptime)
self.send_response(200)
self.end_headers()
self.wfile.write(b'this is not an ACME key authorization')
except:
pass
def test_http_challenge_timeout():
"""
test_http_challenge_timeout tests that the VA times out challenge requests
to a slow HTTP server appropriately.
"""
# Start a simple python HTTP server on port 5002 in its own thread.
# NOTE(@cpu): The pebble-challtestsrv binds 10.77.77.77:5002 for HTTP-01
# challenges so we must use the 10.88.88.88 address for the throw away
# server for this test and add a mock DNS entry that directs the VA to it.
httpd = HTTPServer(('10.88.88.88', 5002), SlowHTTPRequestHandler)
thread = threading.Thread(target = httpd.serve_forever)
thread.daemon = False
thread.start()
# Pick a random domains
hostname = random_domain()
# Add A record for the domains to ensure the VA's requests are directed
# to the interface that we bound the HTTPServer to.
challSrv.add_a_record(hostname, ["10.88.88.88"])
start = datetime.datetime.utcnow()
end = 0
try:
# We expect a connection timeout error to occur
chisel.expect_problem("urn:acme:error:connection",
lambda: auth_and_issue([hostname], chall_type="http-01"))
end = datetime.datetime.utcnow()
finally:
# Shut down the HTTP server gracefully and join on its thread.
httpd.shutdown()
httpd.server_close()
thread.join()
delta = end - start
# Expected duration should be the RA->VA timeout plus some padding (At
# present the timeout is 2s so adding 4s of padding = 6s)
expectedDuration = 6
if delta.total_seconds() == 0 or delta.total_seconds() > expectedDuration:
raise Exception("expected timeout to occur in under {0} seconds. Took {1}".format(expectedDuration, delta.total_seconds()))
def test_tls_alpn_challenge():
# Pick two random domains
domains = [random_domain(), random_domain()]

View File

@ -97,7 +97,7 @@ def start(race_detection, fakeclock=None, account_uri=None):
# interface and TLS-ALPN-01 responses on 5001 for another interface. The
# choice of which is used is controlled by mock DNS data added by the
# relevant integration tests.
[8053, 'pebble-challtestsrv --defaultIPv4 %s --defaultIPv6 "" --dns01 :8053,:8054 --management :8055 --http01 :5002 -https01 10.77.77.77:5001 --tlsalpn01 10.88.88.88:5001' % os.environ.get("FAKE_DNS")],
[8053, 'pebble-challtestsrv --defaultIPv4 %s --defaultIPv6 "" --dns01 :8053,:8054 --management :8055 --http01 10.77.77.77:5002 -https01 10.77.77.77:5001 --tlsalpn01 10.88.88.88:5001' % os.environ.get("FAKE_DNS")],
[8004, './bin/boulder-va --config %s --addr va1.boulder:9092 --debug-addr :8004' % os.path.join(default_config_dir, "va.json")],
[8104, './bin/boulder-va --config %s --addr va2.boulder:9092 --debug-addr :8104' % os.path.join(default_config_dir, "va.json")],
[8001, './bin/boulder-ca --config %s --ca-addr ca1.boulder:9093 --ocsp-addr ca1.boulder:9096 --debug-addr :8001' % os.path.join(default_config_dir, "ca-a.json")],

View File

@ -33,6 +33,7 @@ import (
"github.com/letsencrypt/boulder/bdns"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/features"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/metrics/mock_metrics"
@ -378,33 +379,58 @@ func TestHTTPTimeout(t *testing.T) {
// TODO(#1989): close hs
va, _ := setup(hs, 0)
setChallengeToken(&chall, pathWaitLong)
started := time.Now()
timeout := 50 * time.Millisecond
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
testCases := []struct {
Name string
SimplifiedVAHTTP bool
}{
{"Legacy VA HTTP", false},
{"Simplified VA HTTP", true},
}
_, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
if prob == nil {
t.Fatalf("Connection should've timed out")
}
took := time.Since(started)
// Check that the HTTP connection doesn't return before a timeout, and times
// out after the expected time
if took < timeout {
t.Fatalf("HTTP timed out before %s: %s with %s", timeout, took, prob)
}
if took > 2*timeout {
t.Fatalf("HTTP connection didn't timeout after %s", timeout)
}
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
expectMatch := regexp.MustCompile(
"Fetching http://localhost:\\d+/.well-known/acme-challenge/wait-long: Timeout after connect")
if !expectMatch.MatchString(prob.Detail) {
t.Errorf("Problem details incorrect. Got %q, expected to match %q",
prob.Detail, expectMatch)
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
var expectMatch *regexp.Regexp
expectMatch = regexp.MustCompile(
"Fetching http://localhost:\\d+/.well-known/acme-challenge/wait-long: Timeout after connect")
if tc.SimplifiedVAHTTP {
err := features.Set(map[string]bool{"SimplifiedVAHTTP": true})
test.AssertNotError(t, err, "Failed to set SimplifiedVAHTTP feature flag")
defer features.Reset()
// Simplified VA HTTP error messages don't include the port number when
// it is equal to the va http port since it is implied by the `http://`
// protocol prefix.
expectMatch = regexp.MustCompile(
"Fetching http://localhost/.well-known/acme-challenge/wait-long: Timeout after connect")
}
started := time.Now()
timeout := 50 * time.Millisecond
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
_, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
if prob == nil {
t.Fatalf("Connection should've timed out")
}
took := time.Since(started)
// Check that the HTTP connection doesn't return before a timeout, and times
// out after the expected time
if took < timeout {
t.Fatalf("HTTP timed out before %s: %s with %s", timeout, took, prob)
}
if took > 2*timeout {
t.Fatalf("HTTP connection didn't timeout after %s", timeout)
}
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
if !expectMatch.MatchString(prob.Detail) {
t.Errorf("Problem details incorrect. Got %q, expected to match %q",
prob.Detail, expectMatch)
}
})
}
}