From 2ec451d00baba8056c933ea5f3e87977bc230442 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 24 Jun 2015 15:48:27 +0000 Subject: [PATCH] IConfig.simple_http_port (fixes #542). --- acme/challenges.py | 16 +++++++++++++++- acme/challenges_test.py | 11 +++++++++++ letsencrypt/cli.py | 4 +++- letsencrypt/interfaces.py | 2 ++ letsencrypt/plugins/manual.py | 11 +++++++---- letsencrypt/plugins/manual_test.py | 5 +++-- 6 files changed, 41 insertions(+), 8 deletions(-) diff --git a/acme/challenges.py b/acme/challenges.py index e15893006..0a2a461f0 100644 --- a/acme/challenges.py +++ b/acme/challenges.py @@ -88,6 +88,11 @@ class SimpleHTTPResponse(ChallengeResponse): """URL scheme for the provisioned resource.""" return "https" if self.tls else "http" + @property + def port(self): + """Port that the ACME client should be listening for validation.""" + return 443 if self.tls else 80 + def uri(self, domain): """Create an URI to the provisioned resource. @@ -100,7 +105,7 @@ class SimpleHTTPResponse(ChallengeResponse): return self._URI_TEMPLATE.format( scheme=self.scheme, domain=domain, path=self.path) - def simple_verify(self, chall, domain): + def simple_verify(self, chall, domain, port=None): """Simple verify. According to the ACME specification, "the ACME server MUST @@ -109,12 +114,21 @@ class SimpleHTTPResponse(ChallengeResponse): :param .SimpleHTTP chall: Corresponding challenge. :param str domain: Domain name being verified. + :param int port: Port used in the validation. :returns: ``True`` iff validation is successful, ``False`` otherwise. :rtype: bool """ + # TODO: ACME specification defines URI template that doesn't + # allow to use a custom port... Make sure port is not in the + # request URI, if it's standard. + if port is not None and port != self.port: + logger.warn( + "Using non-standard port for SimpleHTTP verification: %s", port) + domain += ":{0}".format(port) + uri = self.uri(domain) logger.debug("Verifying %s at %s...", chall.typ, uri) try: diff --git a/acme/challenges_test.py b/acme/challenges_test.py index 4a3867c44..bd332d1d9 100644 --- a/acme/challenges_test.py +++ b/acme/challenges_test.py @@ -7,6 +7,7 @@ import Crypto.PublicKey.RSA import M2Crypto import mock import requests +import urlparse from acme import jose from acme import other @@ -85,6 +86,10 @@ class SimpleHTTPResponseTest(unittest.TestCase): self.assertEqual('http', self.msg_http.scheme) self.assertEqual('https', self.msg_https.scheme) + def test_port(self): + self.assertEqual(80, self.msg_http.port) + self.assertEqual(443, self.msg_https.port) + def test_uri(self): self.assertEqual( 'http://example.com/.well-known/acme-challenge/' @@ -134,6 +139,12 @@ class SimpleHTTPResponseTest(unittest.TestCase): mock_get.side_effect = requests.exceptions.RequestException self.assertFalse(self.resp_http.simple_verify(self.chall, "local")) + @mock.patch("acme.challenges.requests.get") + def test_simple_verify_port(self, mock_get): + self.resp_http.simple_verify(self.chall, "local", 4430) + self.assertEqual("local:4430", urlparse.urlparse( + mock_get.mock_calls[0][1][0]).netloc) + class DVSNITest(unittest.TestCase): diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index e64044077..5e789bfb3 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -480,9 +480,11 @@ def create_parser(plugins, args): "testing", "--no-verify-ssl", action="store_true", help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) - helpful.add( # TODO: apache and nginx plugins do NOT respect it (#479) + helpful.add( # TODO: apache and nginx plugins do NOT respect it (#479) "testing", "--dvsni-port", type=int, default=flag_default("dvsni_port"), help=config_help("dvsni_port")) + helpful.add("testing", "--simple-http-port", type=int, + help=config_help("simple_http_port")) helpful.add("testing", "--no-simple-http-tls", action="store_true", help=config_help("no_simple_http_tls")) diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 38ec4ada0..4b93757c8 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -188,6 +188,8 @@ class IConfig(zope.interface.Interface): no_simple_http_tls = zope.interface.Attribute( "Do not use TLS when solving SimpleHTTP challenges.") + simple_http_port = zope.interface.Attribute( + "Port used in the SimpleHttp challenge.") class IInstaller(IPlugin): diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index eff5e7784..700759194 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -49,7 +49,7 @@ echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path} # run only once per server: python -c "import BaseHTTPServer, SimpleHTTPServer; \\ SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {{'': '{ct}'}}; \\ -s = BaseHTTPServer.HTTPServer(('', 80), SimpleHTTPServer.SimpleHTTPRequestHandler); \\ +s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\ s.serve_forever()" """ """Non-TLS command template.""" @@ -62,7 +62,7 @@ echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path} openssl req -new -newkey rsa:4096 -subj "/" -days 1 -nodes -x509 -keyout ../key.pem -out ../cert.pem python -c "import BaseHTTPServer, SimpleHTTPServer, ssl; \\ SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {{'': '{ct}'}}; \\ -s = BaseHTTPServer.HTTPServer(('', 443), SimpleHTTPServer.SimpleHTTPRequestHandler); \\ +s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\ s.socket = ssl.wrap_socket(s.socket, keyfile='../key.pem', certfile='../cert.pem'); \\ s.serve_forever()" """ """TLS command template. @@ -113,9 +113,12 @@ binary for temporary key/certificate generation.""".replace("\n", "") self._notify_and_wait(self.MESSAGE_TEMPLATE.format( achall=achall, response=response, uri=response.uri(achall.domain), ct=response.CONTENT_TYPE, command=self.template.format( - achall=achall, response=response, ct=response.CONTENT_TYPE))) + achall=achall, response=response, ct=response.CONTENT_TYPE, + port=(response.port if self.config.simple_http_port is None + else self.config.simple_http_port)))) - if response.simple_verify(achall.challb, achall.domain): + if response.simple_verify( + achall.challb, achall.domain, self.config.simple_http_port): return response else: return None diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index d412735bf..a533bcc75 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -14,7 +14,8 @@ class ManualAuthenticatorTest(unittest.TestCase): def setUp(self): from letsencrypt.plugins.manual import ManualAuthenticator - self.config = mock.MagicMock(no_simple_http_tls=True) + self.config = mock.MagicMock( + no_simple_http_tls=True, simple_http_port=4430) self.auth = ManualAuthenticator(config=self.config, name="manual") self.achalls = [achallenges.SimpleHTTP( challb=acme_util.SIMPLE_HTTP, domain="foo.com", key=None)] @@ -41,7 +42,7 @@ class ManualAuthenticatorTest(unittest.TestCase): resp = challenges.SimpleHTTPResponse(tls=False, path='Zm9v') self.assertEqual([resp], self.auth.perform(self.achalls)) mock_raw_input.assert_called_once() - mock_verify.assert_called_with(self.achalls[0].challb, "foo.com") + mock_verify.assert_called_with(self.achalls[0].challb, "foo.com", 4430) message = mock_stdout.write.mock_calls[0][1][0] self.assertTrue(self.achalls[0].token in message)