Skip to content

Commit 1c0be52

Browse files
cpujsha
authored andcommitted
VA: Add integration test for HTTP timeouts. (letsencrypt#4050)
Also update `TestHTTPTimeout` to test with the `SimplifiedVAHTTP` feature flag enabled.
1 parent c37355b commit 1c0be52

6 files changed

Lines changed: 113 additions & 28 deletions

File tree

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ services:
44
# To minimize fetching this should be the same version used below
55
image: letsencrypt/boulder-tools-go${TRAVIS_GO_VERSION:-1.11.5}:2019-02-11
66
environment:
7-
FAKE_DNS: 127.0.0.1
7+
FAKE_DNS: 10.77.77.77
88
PKCS11_PROXY_SOCKET: tcp://boulder-hsm:5657
99
BOULDER_CONFIG_DIR: test/config
1010
volumes:

test/config-next/ra.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
},
1919
"vaService": {
2020
"serverAddress": "va.boulder:9092",
21-
"timeout": "20s"
21+
"timeout": "2s"
2222
},
2323
"caService": {
2424
"serverAddress": "ca.boulder:9093",

test/config/ra.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
},
2424
"vaService": {
2525
"serverAddress": "va.boulder:9092",
26-
"timeout": "20s"
26+
"timeout": "2s"
2727
},
2828
"caService": {
2929
"serverAddress": "ca.boulder:9093",

test/integration-test.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,65 @@ def test_http_challenge_https_redirect():
306306
elif r['ServerName'] != d:
307307
raise Exception("Expected all redirected requests to have ServerName {0} got \"{1}\"".format(d, r['ServerName']))
308308

309+
import threading
310+
from http.server import HTTPServer, BaseHTTPRequestHandler
311+
312+
class SlowHTTPRequestHandler(BaseHTTPRequestHandler):
313+
def do_GET(self):
314+
try:
315+
# Sleeptime needs to be larger than the RA->VA timeout (2s at the
316+
# time of writing)
317+
sleeptime = 5
318+
print("SlowHTTPRequestHandler: sleeping for {0}s\n".format(sleeptime))
319+
time.sleep(sleeptime)
320+
self.send_response(200)
321+
self.end_headers()
322+
self.wfile.write(b'this is not an ACME key authorization')
323+
except:
324+
pass
325+
326+
def test_http_challenge_timeout():
327+
"""
328+
test_http_challenge_timeout tests that the VA times out challenge requests
329+
to a slow HTTP server appropriately.
330+
"""
331+
# Start a simple python HTTP server on port 5002 in its own thread.
332+
# NOTE(@cpu): The pebble-challtestsrv binds 10.77.77.77:5002 for HTTP-01
333+
# challenges so we must use the 10.88.88.88 address for the throw away
334+
# server for this test and add a mock DNS entry that directs the VA to it.
335+
httpd = HTTPServer(('10.88.88.88', 5002), SlowHTTPRequestHandler)
336+
thread = threading.Thread(target = httpd.serve_forever)
337+
thread.daemon = False
338+
thread.start()
339+
340+
# Pick a random domains
341+
hostname = random_domain()
342+
343+
# Add A record for the domains to ensure the VA's requests are directed
344+
# to the interface that we bound the HTTPServer to.
345+
challSrv.add_a_record(hostname, ["10.88.88.88"])
346+
347+
start = datetime.datetime.utcnow()
348+
end = 0
349+
350+
try:
351+
# We expect a connection timeout error to occur
352+
chisel.expect_problem("urn:acme:error:connection",
353+
lambda: auth_and_issue([hostname], chall_type="http-01"))
354+
end = datetime.datetime.utcnow()
355+
finally:
356+
# Shut down the HTTP server gracefully and join on its thread.
357+
httpd.shutdown()
358+
httpd.server_close()
359+
thread.join()
360+
361+
delta = end - start
362+
# Expected duration should be the RA->VA timeout plus some padding (At
363+
# present the timeout is 2s so adding 4s of padding = 6s)
364+
expectedDuration = 6
365+
if delta.total_seconds() == 0 or delta.total_seconds() > expectedDuration:
366+
raise Exception("expected timeout to occur in under {0} seconds. Took {1}".format(expectedDuration, delta.total_seconds()))
367+
309368
def test_tls_alpn_challenge():
310369
# Pick two random domains
311370
domains = [random_domain(), random_domain()]

test/startservers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def start(race_detection, fakeclock=None, account_uri=None):
9797
# interface and TLS-ALPN-01 responses on 5001 for another interface. The
9898
# choice of which is used is controlled by mock DNS data added by the
9999
# relevant integration tests.
100-
[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")],
100+
[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")],
101101
[8004, './bin/boulder-va --config %s --addr va1.boulder:9092 --debug-addr :8004' % os.path.join(default_config_dir, "va.json")],
102102
[8104, './bin/boulder-va --config %s --addr va2.boulder:9092 --debug-addr :8104' % os.path.join(default_config_dir, "va.json")],
103103
[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")],

va/va_test.go

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/letsencrypt/boulder/bdns"
3434
"github.com/letsencrypt/boulder/cmd"
3535
"github.com/letsencrypt/boulder/core"
36+
"github.com/letsencrypt/boulder/features"
3637
blog "github.com/letsencrypt/boulder/log"
3738
"github.com/letsencrypt/boulder/metrics"
3839
"github.com/letsencrypt/boulder/metrics/mock_metrics"
@@ -378,33 +379,58 @@ func TestHTTPTimeout(t *testing.T) {
378379
// TODO(#1989): close hs
379380

380381
va, _ := setup(hs, 0)
381-
382382
setChallengeToken(&chall, pathWaitLong)
383-
started := time.Now()
384383

385-
timeout := 50 * time.Millisecond
386-
ctx, cancel := context.WithTimeout(context.Background(), timeout)
387-
defer cancel()
384+
testCases := []struct {
385+
Name string
386+
SimplifiedVAHTTP bool
387+
}{
388+
{"Legacy VA HTTP", false},
389+
{"Simplified VA HTTP", true},
390+
}
391+
392+
for _, tc := range testCases {
393+
t.Run(tc.Name, func(t *testing.T) {
394+
var expectMatch *regexp.Regexp
395+
expectMatch = regexp.MustCompile(
396+
"Fetching http://localhost:\\d+/.well-known/acme-challenge/wait-long: Timeout after connect")
397+
398+
if tc.SimplifiedVAHTTP {
399+
err := features.Set(map[string]bool{"SimplifiedVAHTTP": true})
400+
test.AssertNotError(t, err, "Failed to set SimplifiedVAHTTP feature flag")
401+
defer features.Reset()
402+
// Simplified VA HTTP error messages don't include the port number when
403+
// it is equal to the va http port since it is implied by the `http://`
404+
// protocol prefix.
405+
expectMatch = regexp.MustCompile(
406+
"Fetching http://localhost/.well-known/acme-challenge/wait-long: Timeout after connect")
407+
}
388408

389-
_, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
390-
if prob == nil {
391-
t.Fatalf("Connection should've timed out")
392-
}
393-
took := time.Since(started)
394-
// Check that the HTTP connection doesn't return before a timeout, and times
395-
// out after the expected time
396-
if took < timeout {
397-
t.Fatalf("HTTP timed out before %s: %s with %s", timeout, took, prob)
398-
}
399-
if took > 2*timeout {
400-
t.Fatalf("HTTP connection didn't timeout after %s", timeout)
401-
}
402-
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
403-
expectMatch := regexp.MustCompile(
404-
"Fetching http://localhost:\\d+/.well-known/acme-challenge/wait-long: Timeout after connect")
405-
if !expectMatch.MatchString(prob.Detail) {
406-
t.Errorf("Problem details incorrect. Got %q, expected to match %q",
407-
prob.Detail, expectMatch)
409+
started := time.Now()
410+
timeout := 50 * time.Millisecond
411+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
412+
defer cancel()
413+
_, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
414+
if prob == nil {
415+
t.Fatalf("Connection should've timed out")
416+
}
417+
418+
took := time.Since(started)
419+
// Check that the HTTP connection doesn't return before a timeout, and times
420+
// out after the expected time
421+
if took < timeout {
422+
t.Fatalf("HTTP timed out before %s: %s with %s", timeout, took, prob)
423+
}
424+
if took > 2*timeout {
425+
t.Fatalf("HTTP connection didn't timeout after %s", timeout)
426+
}
427+
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
428+
429+
if !expectMatch.MatchString(prob.Detail) {
430+
t.Errorf("Problem details incorrect. Got %q, expected to match %q",
431+
prob.Detail, expectMatch)
432+
}
433+
})
408434
}
409435
}
410436

0 commit comments

Comments
 (0)