diff --git a/docs/conf.py b/docs/conf.py index fbcd61065..018d2afed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,13 +12,22 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys +import codecs import os +import re +import sys + +here = os.path.abspath(os.path.dirname(__file__)) + +# read version number (and other metadata) from package init +init_fn = os.path.join(here, '..', 'letsencrypt', '__init__.py') +with codecs.open(init_fn, encoding='utf8') as fd: + meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", fd.read())) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath(os.path.join(here, '..'))) # -- General configuration ------------------------------------------------ @@ -58,9 +67,9 @@ copyright = u'2014, Let\'s Encrypt Project' # built documents. # # The short X.Y version. -version = '0.1' +version = '.'.join(meta['version'].split('.')[:2]) # The full version, including alpha/beta/rc tags. -release = '0.1' +release = meta['version'] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/letsencrypt/__init__.py b/letsencrypt/__init__.py index 9fe93c4db..b36747b5f 100644 --- a/letsencrypt/__init__.py +++ b/letsencrypt/__init__.py @@ -1,2 +1,3 @@ """Let's Encrypt.""" +# version number like 1.2.3a0, must have at least 2 parts, like 1.2 __version__ = "0.1" diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index 80d08e5fc..4ba6db2ee 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -129,7 +129,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes self._cleanup_challenges(domain) def _satisfy_challenges(self): - """Attempt to satisfy all saved challenge messages.""" + """Attempt to satisfy all saved challenge messages. + + .. todo:: It might be worth it to try different challenges to + find one that doesn't throw an exception + + """ logging.info("Performing the following challenges:") for dom in self.domains: self.paths[dom] = gen_challenge_path( @@ -149,8 +154,19 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes flat_client.extend(ichall.chall for ichall in self.client_c[dom]) flat_auth.extend(ichall.chall for ichall in self.dv_c[dom]) - client_resp = self.client_auth.perform(flat_client) - dv_resp = self.dv_auth.perform(flat_auth) + try: + client_resp = self.client_auth.perform(flat_client) + dv_resp = self.dv_auth.perform(flat_auth) + # This will catch both specific types of errors. + except errors.LetsEncryptAuthHandlerError as err: + logging.critical("Failure in setting up challenges:") + logging.critical(str(err)) + logging.info("Attempting to clean up outstanding challenges...") + for dom in self.domains: + self._cleanup_challenges(dom) + + raise errors.LetsEncryptAuthHandlerError( + "Unable to perform challenges") logging.info("Ready for verification...") @@ -197,8 +213,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes """ logging.info("Cleaning up challenges for %s", domain) - self.dv_auth.cleanup(self.dv_c[domain]) - self.client_auth.cleanup(self.client_c[domain]) + # These are indexed challenges... give just the challenges to the auth + # Chose to make these lists instead of a generator to make it easier to + # work with... + self.dv_auth.cleanup([ichall.chall for ichall in self.dv_c[domain]]) + self.client_auth.cleanup( + [ichall.chall for ichall in self.client_c[domain]]) def _cleanup_state(self, delete_list): """Cleanup state after an authorization is received. @@ -285,8 +305,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes elif chall["type"] == "dns": logging.info(" DNS challenge for name %s.", domain) - return challenge_util.DnsChall( - domain, str(chall["token"]), self.authkey[domain]) + return challenge_util.DnsChall(domain, str(chall["token"])) else: raise errors.LetsEncryptClientError( diff --git a/letsencrypt/client/challenge_util.py b/letsencrypt/client/challenge_util.py index 4c37cfee2..d05816481 100644 --- a/letsencrypt/client/challenge_util.py +++ b/letsencrypt/client/challenge_util.py @@ -14,7 +14,7 @@ from letsencrypt.client import crypto_util DvsniChall = collections.namedtuple("DvsniChall", "domain, r_b64, nonce, key") SimpleHttpsChall = collections.namedtuple( "SimpleHttpsChall", "domain, token, key") -DnsChall = collections.namedtuple("DnsChall", "domain, token, key") +DnsChall = collections.namedtuple("DnsChall", "domain, token") # Client Challenges RecContactChall = collections.namedtuple( diff --git a/letsencrypt/client/errors.py b/letsencrypt/client/errors.py index 6a3739832..d49611ce7 100644 --- a/letsencrypt/client/errors.py +++ b/letsencrypt/client/errors.py @@ -9,6 +9,7 @@ class LetsEncryptReverterError(LetsEncryptClientError): """Let's Encrypt Reverter error.""" +# Auth Handler Errors class LetsEncryptAuthHandlerError(LetsEncryptClientError): """Let's Encrypt Auth Handler error.""" @@ -17,6 +18,16 @@ class LetsEncryptClientAuthError(LetsEncryptAuthHandlerError): """Let's Encrypt Client Authenticator error.""" +class LetsEncryptDvAuthError(LetsEncryptAuthHandlerError): + """Let's Encrypt DV Authenticator error.""" + + +# Authenticator - Challenge specific errors +class LetsEncryptDvsniError(LetsEncryptDvAuthError): + """Let's Encrypt DVSNI error.""" + + +# Configurator Errors class LetsEncryptConfiguratorError(LetsEncryptClientError): """Let's Encrypt Configurator error.""" @@ -28,6 +39,3 @@ class LetsEncryptNoInstallationError(LetsEncryptConfiguratorError): class LetsEncryptMisconfigurationError(LetsEncryptConfiguratorError): """Let's Encrypt Misconfiguration error.""" - -class LetsEncryptDvsniError(LetsEncryptConfiguratorError): - """Let's Encrypt DVSNI error.""" diff --git a/letsencrypt/client/tests/auth_handler_test.py b/letsencrypt/client/tests/auth_handler_test.py index 28182c7e7..615dfa88b 100644 --- a/letsencrypt/client/tests/auth_handler_test.py +++ b/letsencrypt/client/tests/auth_handler_test.py @@ -1,10 +1,12 @@ """Tests for letsencrypt.client.auth_handler.""" +import logging import unittest import mock from letsencrypt import acme +from letsencrypt.client import challenge_util from letsencrypt.client import errors from letsencrypt.client.tests import acme_util @@ -38,6 +40,11 @@ class SatisfyChallengesTest(unittest.TestCase): self.handler = AuthHandler( self.mock_dv_auth, self.mock_client_auth, None) + logging.disable(logging.CRITICAL) + + def tearDown(self): + logging.disable(logging.NOTSET) + def test_name1_dvsni1(self): dom = "0" challenge = [acme_util.CHALLENGES["dvsni"]] @@ -58,7 +65,7 @@ class SatisfyChallengesTest(unittest.TestCase): def test_name5_dvsni5(self): challenge = [acme_util.CHALLENGES["dvsni"]] - for i in range(5): + for i in xrange(5): self.handler.add_chall_msg( str(i), acme.messages.Challenge(session_id=str(i), nonce="nonce%d" % i, @@ -72,14 +79,14 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual(len(self.handler.client_c), 5) # Each message contains 1 auth, 0 client - for i in range(5): + for i in xrange(5): dom = str(i) self.assertEqual(len(self.handler.responses[dom]), 1) self.assertEqual(self.handler.responses[dom][0], "DvsniChall%d" % i) self.assertEqual(len(self.handler.dv_c[dom]), 1) self.assertEqual(len(self.handler.client_c[dom]), 0) - self.assertEqual( - type(self.handler.dv_c[dom][0].chall).__name__, "DvsniChall") + self.assertTrue(isinstance(self.handler.dv_c[dom][0].chall, + challenge_util.DvsniChall)) @mock.patch("letsencrypt.client.auth_handler.gen_challenge_path") def test_name1_auth(self, mock_chall_path): @@ -108,8 +115,8 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual(len(self.handler.dv_c[dom]), 1) self.assertEqual(len(self.handler.client_c[dom]), 0) - self.assertEqual( - type(self.handler.dv_c[dom][0].chall).__name__, "SimpleHttpsChall") + self.assertTrue(isinstance(self.handler.dv_c[dom][0].chall, + challenge_util.SimpleHttpsChall)) @mock.patch("letsencrypt.client.auth_handler.gen_challenge_path") def test_name1_all(self, mock_chall_path): @@ -138,16 +145,16 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual( self.handler.responses[dom], self._get_exp_response(dom, path, challenges)) - self.assertEqual( - type(self.handler.dv_c[dom][0].chall).__name__, "SimpleHttpsChall") - self.assertEqual( - type(self.handler.client_c[dom][0].chall).__name__, "RecTokenChall") + self.assertTrue(isinstance(self.handler.dv_c[dom][0].chall, + challenge_util.SimpleHttpsChall)) + self.assertTrue(isinstance(self.handler.client_c[dom][0].chall, + challenge_util.RecTokenChall)) @mock.patch("letsencrypt.client.auth_handler.gen_challenge_path") def test_name5_all(self, mock_chall_path): challenges = acme_util.get_challenges() combos = acme_util.gen_combos(challenges) - for i in range(5): + for i in xrange(5): self.handler.add_chall_msg( str(i), acme.messages.Challenge( @@ -161,7 +168,7 @@ class SatisfyChallengesTest(unittest.TestCase): self.handler._satisfy_challenges() # pylint: disable=protected-access self.assertEqual(len(self.handler.responses), 5) - for i in range(5): + for i in xrange(5): self.assertEqual( len(self.handler.responses[str(i)]), len(challenges)) self.assertEqual(len(self.handler.dv_c), 5) @@ -175,11 +182,10 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual(len(self.handler.dv_c[dom]), 1) self.assertEqual(len(self.handler.client_c[dom]), 1) - self.assertEqual( - type(self.handler.dv_c[dom][0].chall).__name__, "DvsniChall") - self.assertEqual( - type(self.handler.client_c[dom][0].chall).__name__, - "RecContactChall") + self.assertTrue(isinstance(self.handler.dv_c[dom][0].chall, + challenge_util.DvsniChall)) + self.assertTrue(isinstance(self.handler.client_c[dom][0].chall, + challenge_util.RecContactChall)) @mock.patch("letsencrypt.client.auth_handler.gen_challenge_path") def test_name5_mix(self, mock_chall_path): @@ -196,7 +202,7 @@ class SatisfyChallengesTest(unittest.TestCase): acme_util.get_challenges()] # Combos doesn't matter since I am overriding the gen_path function - for i in range(5): + for i in xrange(5): dom = str(i) paths.append(gen_path(chosen_chall[i], challenge_list[i])) self.handler.add_chall_msg( @@ -214,7 +220,7 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual(len(self.handler.dv_c), 5) self.assertEqual(len(self.handler.client_c), 5) - for i in range(5): + for i in xrange(5): dom = str(i) resp = self._get_exp_response(i, paths[i], challenge_list[i]) self.assertEqual(self.handler.responses[dom], resp) @@ -222,21 +228,67 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual( len(self.handler.client_c[dom]), len(chosen_chall[i]) - 1) - self.assertEqual( - type(self.handler.dv_c["0"][0].chall).__name__, "DnsChall") - self.assertEqual( - type(self.handler.dv_c["1"][0].chall).__name__, "DvsniChall") - self.assertEqual( - type(self.handler.dv_c["2"][0].chall).__name__, "SimpleHttpsChall") - self.assertEqual( - type(self.handler.dv_c["3"][0].chall).__name__, "SimpleHttpsChall") - self.assertEqual( - type(self.handler.dv_c["4"][0].chall).__name__, "DnsChall") + self.assertTrue(isinstance(self.handler.dv_c["0"][0].chall, + challenge_util.DnsChall)) + self.assertTrue(isinstance(self.handler.dv_c["1"][0].chall, + challenge_util.DvsniChall)) + self.assertTrue(isinstance(self.handler.dv_c["2"][0].chall, + challenge_util.SimpleHttpsChall)) + self.assertTrue(isinstance(self.handler.dv_c["3"][0].chall, + challenge_util.SimpleHttpsChall)) + self.assertTrue(isinstance(self.handler.dv_c["4"][0].chall, + challenge_util.DnsChall)) + + self.assertTrue(isinstance(self.handler.client_c["2"][0].chall, + challenge_util.PopChall)) + self.assertTrue(isinstance(self.handler.client_c["4"][0].chall, + challenge_util.RecTokenChall)) + + @mock.patch("letsencrypt.client.auth_handler.gen_challenge_path") + def test_perform_exception_cleanup(self, mock_chall_path): + """3 Challenge messages... fail perform... clean up.""" + # pylint: disable=protected-access + self.mock_dv_auth.perform.side_effect = errors.LetsEncryptDvsniError + + challenges = acme_util.get_challenges() + combos = acme_util.gen_combos(challenges) + + for i in xrange(3): + self.handler.add_chall_msg( + str(i), + acme.messages.Challenge( + session_id=str(i), nonce="nonce%d" % i, + challenges=challenges, combinations=combos), + "dummy_key") + + mock_chall_path.return_value = gen_path( + ["dvsni", "proofOfPossession"], challenges) + + # This may change in the future... but for now catch the error + self.assertRaises(errors.LetsEncryptAuthHandlerError, + self.handler._satisfy_challenges) + + # Verify cleanup is actually run correctly + self.assertEqual(self.mock_dv_auth.cleanup.call_count, 3) + self.assertEqual(self.mock_client_auth.cleanup.call_count, 3) + + # Check DV cleanup + mock_cleanup_args = self.mock_dv_auth.cleanup.call_args_list + for i in xrange(3): + # Assert length of arg list was 1 + arg_chall_list = mock_cleanup_args[i][0][0] + self.assertEqual(len(arg_chall_list), 1) + self.assertTrue(isinstance(arg_chall_list[0], + challenge_util.DvsniChall)) + + # Check Auth cleanup + mock_cleanup_args = self.mock_client_auth.cleanup.call_args_list + for i in xrange(3): + arg_chall_list = mock_cleanup_args[i][0][0] + self.assertEqual(len(arg_chall_list), 1) + self.assertTrue(isinstance(arg_chall_list[0], + challenge_util.PopChall)) - self.assertEqual( - type(self.handler.client_c["2"][0].chall).__name__, "PopChall") - self.assertEqual( - type(self.handler.client_c["4"][0].chall).__name__, "RecTokenChall") def _get_exp_response(self, domain, path, challenges): # pylint: disable=no-self-use @@ -269,7 +321,7 @@ class GetAuthorizationsTest(unittest.TestCase): def test_solved3_at_once(self): # Set 3 DVSNI challenges challenge = [acme_util.CHALLENGES["dvsni"]] - for i in range(3): + for i in xrange(3): self.handler.add_chall_msg( str(i), acme.messages.Challenge(session_id=str(i), nonce="nonce%d" % i, @@ -288,7 +340,7 @@ class GetAuthorizationsTest(unittest.TestCase): self._test_finished() def _sat_solved_at_once(self): - for i in range(3): + for i in xrange(3): dom = str(i) self.handler.responses[dom] = ["DvsniChall%d" % i] self.handler.paths[dom] = [0] @@ -326,7 +378,7 @@ class GetAuthorizationsTest(unittest.TestCase): challs = [] challs.append(acme_util.get_challenges()) challs.append(acme_util.get_dv_challenges()) - for i in range(2): + for i in xrange(2): dom = str(i) self.handler.add_chall_msg( dom, @@ -401,7 +453,7 @@ class PathSatisfiedTest(unittest.TestCase): self.handler.paths[dom[4]] = [] self.handler.responses[dom[4]] = ["respond... sure"] - for i in range(5): + for i in xrange(5): self.assertTrue(self.handler._path_satisfied(dom[i])) def test_not_satisfied(self): @@ -418,16 +470,25 @@ class PathSatisfiedTest(unittest.TestCase): self.handler.paths[dom[3]] = [0] self.handler.responses[dom[3]] = ["null"] - for i in range(4): + for i in xrange(4): self.assertFalse(self.handler._path_satisfied(dom[i])) -def gen_auth_resp(chall_list): # pylint: disable=missing-docstring +def gen_auth_resp(chall_list): + """Generate a dummy authorization response.""" return ["%s%s" % (type(chall).__name__, chall.domain) for chall in chall_list] -def gen_path(str_list, challenges): # pylint: disable=missing-docstring +def gen_path(str_list, challenges): + """Generate a path for challenge messages + + :param list str_list: challenge message types (:class:`str`) + :param dict challenges: ACME challenge messages + + :return: :class:`list` of :class:`int` + + """ path = [] for i, chall in enumerate(challenges): for str_chall in str_list: diff --git a/letsencrypt/client/tests/le_util_test.py b/letsencrypt/client/tests/le_util_test.py index 6dcbf57e7..39926a9b5 100644 --- a/letsencrypt/client/tests/le_util_test.py +++ b/letsencrypt/client/tests/le_util_test.py @@ -1,6 +1,7 @@ """Tests for letsencrypt.client.le_util.""" import os import shutil +import stat import tempfile import unittest @@ -33,11 +34,11 @@ class MakeOrVerifyDirTest(unittest.TestCase): path = os.path.join(self.root_path, 'bar') self._call(path, 0o650) self.assertTrue(os.path.isdir(path)) - # TODO: check mode + self.assertEqual(stat.S_IMODE(os.stat(path).st_mode), 0o650) def test_existing_correct_mode_does_not_fail(self): self._call(self.path, 0o400) - # TODO: check mode + self.assertEqual(stat.S_IMODE(os.stat(self.path).st_mode), 0o400) def test_existing_wrong_mode_fails(self): self.assertRaises(Exception, self._call, self.path, 0o600)