diff --git a/test/v2_integration.py b/test/v2_integration.py index e48842110..42eb026d3 100644 --- a/test/v2_integration.py +++ b/test/v2_integration.py @@ -63,6 +63,43 @@ def rand_http_chall(client): return d, c.chall raise Exception("No HTTP-01 challenge found for random domain authz") +def test_http_challenge_broken_redirect(): + """ + test_http_challenge_broken_redirect tests that a common webserver + mis-configuration receives the correct specialized error message when attempting + an HTTP-01 challenge. + """ + client = chisel2.make_client() + + # Create an authz for a random domain and get its HTTP-01 challenge token + d, chall = rand_http_chall(client) + token = chall.encode("token") + + # Create a broken HTTP redirect similar to a sort we see frequently "in the wild" + challengePath = "/.well-known/acme-challenge/{0}".format(token) + redirect = "http://{0}.well-known/acme-challenge/bad-bad-bad".format(d) + challSrv.add_http_redirect( + challengePath, + redirect) + + # Expect the specialized error message + expectedError = "Fetching {0}: Invalid host in redirect target \"{1}.well-known\". Check webserver config for missing '/' in redirect target.".format(redirect, d) + + # NOTE(@cpu): Can't use chisel2.expect_problem here because it doesn't let + # us interrogate the detail message easily. + try: + chisel2.auth_and_issue([d], client=client, chall_type="http-01") + except acme_errors.ValidationError as e: + for authzr in e.failed_authzrs: + c = chisel2.get_chall(authzr, challenges.HTTP01) + error = c.error + if error is None or error.typ != "urn:ietf:params:acme:error:connection": + raise Exception("Expected connection prob, got %s" % (error.__str__())) + if error.detail != expectedError: + raise Exception("Expected prob detail %s, got %s" % (expectedError, error.detail)) + + challSrv.remove_http_redirect(challengePath) + def test_http_challenge_loop_redirect(): client = chisel2.make_client() diff --git a/va/http.go b/va/http.go index c2a96799e..3032374d3 100644 --- a/va/http.go +++ b/va/http.go @@ -314,6 +314,22 @@ func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (stri "Only domain names are supported, not IP addresses", reqHost) } + // Often folks will misconfigure their webserver to send an HTTP redirect + // missing a `/' between the FQDN and the path. E.g. in Apache using: + // Redirect / https://bad-redirect.org + // Instead of + // Redirect / https://bad-redirect.org/ + // Will produce an invalid HTTP-01 redirect target like: + // https://bad-redirect.org.well-known/acme-challenge/xxxx + // This happens frequently enough we want to return a distinct error message + // for this case by detecting the reqHost ending in ".well-known". + if strings.HasSuffix(reqHost, ".well-known") { + return "", 0, berrors.ConnectionFailureError( + "Invalid host in redirect target %q. Check webserver config for missing '/' in redirect target.", + reqHost, + ) + } + if _, err := iana.ExtractSuffix(reqHost); err != nil { return "", 0, berrors.ConnectionFailureError( "Invalid hostname in redirect target, must end in IANA registered TLD") diff --git a/va/http_test.go b/va/http_test.go index 18780b695..ce2165451 100644 --- a/va/http_test.go +++ b/va/http_test.go @@ -232,6 +232,13 @@ func TestExtractRequestTarget(t *testing.T) { }, ExpectedError: errors.New("Invalid empty hostname in redirect target"), }, + { + Name: "invalid .well-known hostname", + Req: &http.Request{ + URL: mustURL(t, "https://my.webserver.is.misconfigured.well-known/acme-challenge/xxx"), + }, + ExpectedError: errors.New(`Invalid host in redirect target "my.webserver.is.misconfigured.well-known". Check webserver config for missing '/' in redirect target.`), + }, { Name: "invalid non-iana hostname", Req: &http.Request{