From 26a25a705382f5de40d390e15929ed5f6b0b9b96 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 17 Feb 2016 18:34:37 -0800 Subject: [PATCH 01/23] allow users to choose how many config changes are shown --- letsencrypt/cli.py | 5 ++++- letsencrypt/client.py | 4 ++-- letsencrypt/reverter.py | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 855c7a467..30654ea06 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1059,7 +1059,7 @@ def config_changes(config, unused_plugins): View checkpoints and associated configuration changes. """ - client.view_config_changes(config) + client.view_config_changes(config, num=config.num) def plugins_cmd(config, plugins): # TODO: Use IDisplay rather than print @@ -1633,6 +1633,9 @@ def _create_subparsers(helpful): helpful.add_group("revoke", description="Options for revocation of certs") helpful.add_group("rollback", description="Options for reverting config changes") helpful.add_group("plugins", description="Plugin options") + helpful.add_group("config_changes", description="Options for showing a history of config changes") + helpful.add("config_changes", "--num", type=int, + help="How many past revisions you want to be displayed") helpful.add( None, "--user-agent", default=None, help="Set a custom user agent string for the client. User agent strings allow " diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 9dfa70e8d..149fdbbe9 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -543,7 +543,7 @@ def rollback(default_installer, checkpoints, config, plugins): installer.restart() -def view_config_changes(config): +def view_config_changes(config, num=None): """View checkpoints and associated configuration changes. .. note:: This assumes that the installation is using a Reverter object. @@ -554,7 +554,7 @@ def view_config_changes(config): """ rev = reverter.Reverter(config) rev.recovery_routine() - rev.view_config_changes() + rev.view_config_changes(num) def _save_chain(chain_pem, chain_path): diff --git a/letsencrypt/reverter.py b/letsencrypt/reverter.py index 863074374..ea54a91ee 100644 --- a/letsencrypt/reverter.py +++ b/letsencrypt/reverter.py @@ -94,7 +94,7 @@ class Reverter(object): "Unable to load checkpoint during rollback") rollback -= 1 - def view_config_changes(self, for_logging=False): + def view_config_changes(self, for_logging=False, num=None): """Displays all saved checkpoints. All checkpoints are printed by @@ -107,7 +107,8 @@ class Reverter(object): """ backups = os.listdir(self.config.backup_dir) backups.sort(reverse=True) - + if num: + backups = backups[:num] if not backups: logger.info("The Let's Encrypt client has not saved any backups " "of your configuration") From 6a7c3ada654bee5401ae268f8e3160e8e3ccc0a1 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 18 Feb 2016 14:49:57 -0800 Subject: [PATCH 02/23] lint fix --- letsencrypt/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 30654ea06..0422a8c6c 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1633,7 +1633,8 @@ def _create_subparsers(helpful): helpful.add_group("revoke", description="Options for revocation of certs") helpful.add_group("rollback", description="Options for reverting config changes") helpful.add_group("plugins", description="Plugin options") - helpful.add_group("config_changes", description="Options for showing a history of config changes") + helpful.add_group("config_changes", + description="Options for showing a history of config changes") helpful.add("config_changes", "--num", type=int, help="How many past revisions you want to be displayed") helpful.add( From cf12f8702761a14ce55d89a2f75c23a996032c83 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 1 Mar 2016 13:16:19 -0800 Subject: [PATCH 03/23] Remove SIGFILEBALL after creating sig --- tools/offline-sigrequest.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/offline-sigrequest.sh b/tools/offline-sigrequest.sh index 7706796ef..08a5c4c05 100755 --- a/tools/offline-sigrequest.sh +++ b/tools/offline-sigrequest.sh @@ -42,6 +42,7 @@ function oncesigned { # $1 <-- INPFILE ; $2 <--SIGFILE echo `file $2` exit 1 fi + rm $SIGFILEBALL } HERE=`dirname $0` From f7d862d0bf2020116db5cfd5e685e683f44d1cc2 Mon Sep 17 00:00:00 2001 From: Wang Yu Date: Sat, 5 Mar 2016 20:33:02 +0100 Subject: [PATCH 04/23] webroot configuration text--fix format --- docs/using.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 37fca2c57..49d48a974 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -111,7 +111,9 @@ potentially be a separate directory for each domain. When requested a certificate for multiple domains, each domain will use the most recently specified ``--webroot-path``. So, for instance, -``letsencrypt certonly --webroot -w /var/www/example/ -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net`` +:: + + letsencrypt certonly --webroot -w /var/www/example/ -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net would obtain a single certificate for all of those names, using the ``/var/www/example`` webroot directory for the first two, and From a941b6830d806fa20f8bfe47e041ba062d1345df Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 7 Mar 2016 18:42:44 -0800 Subject: [PATCH 05/23] remove crufty continuity challenges --- acme/acme/challenges.py | 105 ---------------- acme/acme/challenges_test.py | 228 ----------------------------------- acme/acme/messages_test.py | 8 +- 3 files changed, 3 insertions(+), 338 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 13d19d3c4..0b15e7fb4 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -13,7 +13,6 @@ from acme import errors from acme import crypto_util from acme import fields from acme import jose -from acme import other logger = logging.getLogger(__name__) @@ -36,10 +35,6 @@ class Challenge(jose.TypedJSONObjectWithFields): return UnrecognizedChallenge.from_json(jobj) -class ContinuityChallenge(Challenge): # pylint: disable=abstract-method - """Client validation challenges.""" - - class DVChallenge(Challenge): # pylint: disable=abstract-method """Domain validation challenges.""" @@ -460,106 +455,6 @@ class TLSSNI01(KeyAuthorizationChallenge): return self.response(account_key).gen_cert(key=kwargs.get('cert_key')) -@Challenge.register -class RecoveryContact(ContinuityChallenge): - """ACME "recoveryContact" challenge. - - :ivar unicode activation_url: - :ivar unicode success_url: - :ivar unicode contact: - - """ - typ = "recoveryContact" - - activation_url = jose.Field("activationURL", omitempty=True) - success_url = jose.Field("successURL", omitempty=True) - contact = jose.Field("contact", omitempty=True) - - -@ChallengeResponse.register -class RecoveryContactResponse(ChallengeResponse): - """ACME "recoveryContact" challenge response. - - :ivar unicode token: - - """ - typ = "recoveryContact" - token = jose.Field("token", omitempty=True) - - -@Challenge.register -class ProofOfPossession(ContinuityChallenge): - """ACME "proofOfPossession" challenge. - - :ivar .JWAAlgorithm alg: - :ivar bytes nonce: Random data, **not** base64-encoded. - :ivar hints: Various clues for the client (:class:`Hints`). - - """ - typ = "proofOfPossession" - - NONCE_SIZE = 16 - - class Hints(jose.JSONObjectWithFields): - """Hints for "proofOfPossession" challenge. - - :ivar JWK jwk: JSON Web Key - :ivar tuple cert_fingerprints: `tuple` of `unicode` - :ivar tuple certs: Sequence of :class:`acme.jose.ComparableX509` - certificates. - :ivar tuple subject_key_identifiers: `tuple` of `unicode` - :ivar tuple issuers: `tuple` of `unicode` - :ivar tuple authorized_for: `tuple` of `unicode` - - """ - jwk = jose.Field("jwk", decoder=jose.JWK.from_json) - cert_fingerprints = jose.Field( - "certFingerprints", omitempty=True, default=()) - certs = jose.Field("certs", omitempty=True, default=()) - subject_key_identifiers = jose.Field( - "subjectKeyIdentifiers", omitempty=True, default=()) - serial_numbers = jose.Field("serialNumbers", omitempty=True, default=()) - issuers = jose.Field("issuers", omitempty=True, default=()) - authorized_for = jose.Field("authorizedFor", omitempty=True, default=()) - - @certs.encoder - def certs(value): # pylint: disable=missing-docstring,no-self-argument - return tuple(jose.encode_cert(cert) for cert in value) - - @certs.decoder - def certs(value): # pylint: disable=missing-docstring,no-self-argument - return tuple(jose.decode_cert(cert) for cert in value) - - alg = jose.Field("alg", decoder=jose.JWASignature.from_json) - nonce = jose.Field( - "nonce", encoder=jose.encode_b64jose, decoder=functools.partial( - jose.decode_b64jose, size=NONCE_SIZE)) - hints = jose.Field("hints", decoder=Hints.from_json) - - -@ChallengeResponse.register -class ProofOfPossessionResponse(ChallengeResponse): - """ACME "proofOfPossession" challenge response. - - :ivar bytes nonce: Random data, **not** base64-encoded. - :ivar acme.other.Signature signature: Sugnature of this message. - - """ - typ = "proofOfPossession" - - NONCE_SIZE = ProofOfPossession.NONCE_SIZE - - nonce = jose.Field( - "nonce", encoder=jose.encode_b64jose, decoder=functools.partial( - jose.decode_b64jose, size=NONCE_SIZE)) - signature = jose.Field("signature", decoder=other.Signature.from_json) - - def verify(self): - """Verify the challenge.""" - # self.signature is not Field | pylint: disable=no-member - return self.signature.verify(self.nonce) - - @Challenge.register # pylint: disable=too-many-ancestors class DNS(_TokenDVChallenge): """ACME "dns" challenge.""" diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index ef78e1eba..04b7442b0 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -9,7 +9,6 @@ from six.moves.urllib import parse as urllib_parse # pylint: disable=import-err from acme import errors from acme import jose -from acme import other from acme import test_util @@ -324,233 +323,6 @@ class TLSSNI01Test(unittest.TestCase): mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key) -class RecoveryContactTest(unittest.TestCase): - - def setUp(self): - from acme.challenges import RecoveryContact - self.msg = RecoveryContact( - activation_url='https://example.ca/sendrecovery/a5bd99383fb0', - success_url='https://example.ca/confirmrecovery/bb1b9928932', - contact='c********n@example.com') - self.jmsg = { - 'type': 'recoveryContact', - 'activationURL': 'https://example.ca/sendrecovery/a5bd99383fb0', - 'successURL': 'https://example.ca/confirmrecovery/bb1b9928932', - 'contact': 'c********n@example.com', - } - - def test_to_partial_json(self): - self.assertEqual(self.jmsg, self.msg.to_partial_json()) - - def test_from_json(self): - from acme.challenges import RecoveryContact - self.assertEqual(self.msg, RecoveryContact.from_json(self.jmsg)) - - def test_from_json_hashable(self): - from acme.challenges import RecoveryContact - hash(RecoveryContact.from_json(self.jmsg)) - - def test_json_without_optionals(self): - del self.jmsg['activationURL'] - del self.jmsg['successURL'] - del self.jmsg['contact'] - - from acme.challenges import RecoveryContact - msg = RecoveryContact.from_json(self.jmsg) - - self.assertTrue(msg.activation_url is None) - self.assertTrue(msg.success_url is None) - self.assertTrue(msg.contact is None) - self.assertEqual(self.jmsg, msg.to_partial_json()) - - -class RecoveryContactResponseTest(unittest.TestCase): - - def setUp(self): - from acme.challenges import RecoveryContactResponse - self.msg = RecoveryContactResponse(token='23029d88d9e123e') - self.jmsg = { - 'resource': 'challenge', - 'type': 'recoveryContact', - 'token': '23029d88d9e123e', - } - - def test_to_partial_json(self): - self.assertEqual(self.jmsg, self.msg.to_partial_json()) - - def test_from_json(self): - from acme.challenges import RecoveryContactResponse - self.assertEqual( - self.msg, RecoveryContactResponse.from_json(self.jmsg)) - - def test_from_json_hashable(self): - from acme.challenges import RecoveryContactResponse - hash(RecoveryContactResponse.from_json(self.jmsg)) - - def test_json_without_optionals(self): - del self.jmsg['token'] - - from acme.challenges import RecoveryContactResponse - msg = RecoveryContactResponse.from_json(self.jmsg) - - self.assertTrue(msg.token is None) - self.assertEqual(self.jmsg, msg.to_partial_json()) - - -class ProofOfPossessionHintsTest(unittest.TestCase): - - def setUp(self): - jwk = KEY.public_key() - issuers = ( - 'C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA', - 'O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure', - ) - cert_fingerprints = ( - '93416768eb85e33adc4277f4c9acd63e7418fcfe', - '16d95b7b63f1972b980b14c20291f3c0d1855d95', - '48b46570d9fc6358108af43ad1649484def0debf', - ) - subject_key_identifiers = ('d0083162dcc4c8a23ecb8aecbd86120e56fd24e5') - authorized_for = ('www.example.com', 'example.net') - serial_numbers = (34234239832, 23993939911, 17) - - from acme.challenges import ProofOfPossession - self.msg = ProofOfPossession.Hints( - jwk=jwk, issuers=issuers, cert_fingerprints=cert_fingerprints, - certs=(CERT,), subject_key_identifiers=subject_key_identifiers, - authorized_for=authorized_for, serial_numbers=serial_numbers) - - self.jmsg_to = { - 'jwk': jwk, - 'certFingerprints': cert_fingerprints, - 'certs': (jose.encode_b64jose(OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_ASN1, CERT.wrapped)),), - 'subjectKeyIdentifiers': subject_key_identifiers, - 'serialNumbers': serial_numbers, - 'issuers': issuers, - 'authorizedFor': authorized_for, - } - self.jmsg_from = self.jmsg_to.copy() - self.jmsg_from.update({'jwk': jwk.to_json()}) - - def test_to_partial_json(self): - self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) - - def test_from_json(self): - from acme.challenges import ProofOfPossession - self.assertEqual( - self.msg, ProofOfPossession.Hints.from_json(self.jmsg_from)) - - def test_from_json_hashable(self): - from acme.challenges import ProofOfPossession - hash(ProofOfPossession.Hints.from_json(self.jmsg_from)) - - def test_json_without_optionals(self): - for optional in ['certFingerprints', 'certs', 'subjectKeyIdentifiers', - 'serialNumbers', 'issuers', 'authorizedFor']: - del self.jmsg_from[optional] - del self.jmsg_to[optional] - - from acme.challenges import ProofOfPossession - msg = ProofOfPossession.Hints.from_json(self.jmsg_from) - - self.assertEqual(msg.cert_fingerprints, ()) - self.assertEqual(msg.certs, ()) - self.assertEqual(msg.subject_key_identifiers, ()) - self.assertEqual(msg.serial_numbers, ()) - self.assertEqual(msg.issuers, ()) - self.assertEqual(msg.authorized_for, ()) - - self.assertEqual(self.jmsg_to, msg.to_partial_json()) - - -class ProofOfPossessionTest(unittest.TestCase): - - def setUp(self): - from acme.challenges import ProofOfPossession - hints = ProofOfPossession.Hints( - jwk=KEY.public_key(), cert_fingerprints=(), - certs=(), serial_numbers=(), subject_key_identifiers=(), - issuers=(), authorized_for=()) - self.msg = ProofOfPossession( - alg=jose.RS256, hints=hints, - nonce=b'xD\xf9\xb9\xdbU\xed\xaa\x17\xf1y|\x81\x88\x99 ') - - self.jmsg_to = { - 'type': 'proofOfPossession', - 'alg': jose.RS256, - 'nonce': 'eET5udtV7aoX8Xl8gYiZIA', - 'hints': hints, - } - self.jmsg_from = { - 'type': 'proofOfPossession', - 'alg': jose.RS256.to_json(), - 'nonce': 'eET5udtV7aoX8Xl8gYiZIA', - 'hints': hints.to_json(), - } - - def test_to_partial_json(self): - self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) - - def test_from_json(self): - from acme.challenges import ProofOfPossession - self.assertEqual( - self.msg, ProofOfPossession.from_json(self.jmsg_from)) - - def test_from_json_hashable(self): - from acme.challenges import ProofOfPossession - hash(ProofOfPossession.from_json(self.jmsg_from)) - - -class ProofOfPossessionResponseTest(unittest.TestCase): - - def setUp(self): - # acme-spec uses a confusing example in which both signature - # nonce and challenge nonce are the same, don't make the same - # mistake here... - signature = other.Signature( - alg=jose.RS256, jwk=KEY.public_key(), - sig=b'\xa7\xc1\xe7\xe82o\xbc\xcd\xd0\x1e\x010#Z|\xaf\x15\x83' - b'\x94\x8f#\x9b\nQo(\x80\x15,\x08\xfcz\x1d\xfd\xfd.\xaap' - b'\xfa\x06\xd1\xa2f\x8d8X2>%d\xbd%\xe1T\xdd\xaa0\x18\xde' - b'\x99\x08\xf0\x0e{', - nonce=b'\x99\xc7Q\xb3f2\xbc\xdci\xfe\xd6\x98k\xc67\xdf', - ) - - from acme.challenges import ProofOfPossessionResponse - self.msg = ProofOfPossessionResponse( - nonce=b'xD\xf9\xb9\xdbU\xed\xaa\x17\xf1y|\x81\x88\x99 ', - signature=signature) - - self.jmsg_to = { - 'resource': 'challenge', - 'type': 'proofOfPossession', - 'nonce': 'eET5udtV7aoX8Xl8gYiZIA', - 'signature': signature, - } - self.jmsg_from = { - 'resource': 'challenge', - 'type': 'proofOfPossession', - 'nonce': 'eET5udtV7aoX8Xl8gYiZIA', - 'signature': signature.to_json(), - } - - def test_verify(self): - self.assertTrue(self.msg.verify()) - - def test_to_partial_json(self): - self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) - - def test_from_json(self): - from acme.challenges import ProofOfPossessionResponse - self.assertEqual( - self.msg, ProofOfPossessionResponse.from_json(self.jmsg_from)) - - def test_from_json_hashable(self): - from acme.challenges import ProofOfPossessionResponse - hash(ProofOfPossessionResponse.from_json(self.jmsg_from)) - - class DNSTest(unittest.TestCase): def setUp(self): diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index 8e74826bf..fa558cf4a 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -271,10 +271,8 @@ class AuthorizationTest(unittest.TestCase): ChallengeBody(uri='http://challb2', status=STATUS_VALID, chall=challenges.DNS( token=b'DGyRejmCefe7v4NfDGDKfA')), - ChallengeBody(uri='http://challb3', status=STATUS_VALID, - chall=challenges.RecoveryContact()), ) - combinations = ((0, 2), (1, 2)) + combinations = ((0,), (1,)) from acme.messages import Authorization from acme.messages import Identifier @@ -300,8 +298,8 @@ class AuthorizationTest(unittest.TestCase): def test_resolved_combinations(self): self.assertEqual(self.authz.resolved_combinations, ( - (self.challbs[0], self.challbs[2]), - (self.challbs[1], self.challbs[2]), + (self.challbs[0],), + (self.challbs[1],), )) From 22a9c7e3c2a811698ed7d63fae1cd43d0bd5d088 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 7 Mar 2016 18:44:30 -0800 Subject: [PATCH 06/23] Remove unused 'other' module --- acme/acme/other.py | 67 ----------------------------- acme/acme/other_test.py | 94 ----------------------------------------- 2 files changed, 161 deletions(-) delete mode 100644 acme/acme/other.py delete mode 100644 acme/acme/other_test.py diff --git a/acme/acme/other.py b/acme/acme/other.py deleted file mode 100644 index edd7210b2..000000000 --- a/acme/acme/other.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Other ACME objects.""" -import functools -import logging -import os - -from acme import jose - - -logger = logging.getLogger(__name__) - - -class Signature(jose.JSONObjectWithFields): - """ACME signature. - - :ivar .JWASignature alg: Signature algorithm. - :ivar bytes sig: Signature. - :ivar bytes nonce: Nonce. - :ivar .JWK jwk: JWK. - - """ - NONCE_SIZE = 16 - """Minimum size of nonce in bytes.""" - - alg = jose.Field('alg', decoder=jose.JWASignature.from_json) - sig = jose.Field('sig', encoder=jose.encode_b64jose, - decoder=jose.decode_b64jose) - nonce = jose.Field( - 'nonce', encoder=jose.encode_b64jose, decoder=functools.partial( - jose.decode_b64jose, size=NONCE_SIZE, minimum=True)) - jwk = jose.Field('jwk', decoder=jose.JWK.from_json) - - @classmethod - def from_msg(cls, msg, key, nonce=None, nonce_size=None, alg=jose.RS256): - """Create signature with nonce prepended to the message. - - :param bytes msg: Message to be signed. - - :param key: Key used for signing. - :type key: `cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey` - (optionally wrapped in `.ComparableRSAKey`). - - :param bytes nonce: Nonce to be used. If None, nonce of - ``nonce_size`` will be randomly generated. - :param int nonce_size: Size of the automatically generated nonce. - Defaults to :const:`NONCE_SIZE`. - - :param .JWASignature alg: - - """ - nonce_size = cls.NONCE_SIZE if nonce_size is None else nonce_size - nonce = os.urandom(nonce_size) if nonce is None else nonce - - msg_with_nonce = nonce + msg - sig = alg.sign(key, nonce + msg) - logger.debug('%r signed as %r', msg_with_nonce, sig) - - return cls(alg=alg, sig=sig, nonce=nonce, - jwk=alg.kty(key=key.public_key())) - - def verify(self, msg): - """Verify the signature. - - :param bytes msg: Message that was used in signing. - - """ - # self.alg is not Field, but JWA | pylint: disable=no-member - return self.alg.verify(self.jwk.key, self.nonce + msg, self.sig) diff --git a/acme/acme/other_test.py b/acme/acme/other_test.py deleted file mode 100644 index 40fad9451..000000000 --- a/acme/acme/other_test.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Tests for acme.sig.""" -import unittest - -from acme import jose -from acme import test_util - - -KEY = test_util.load_rsa_private_key('rsa512_key.pem') - - -class SignatureTest(unittest.TestCase): - # pylint: disable=too-many-instance-attributes - """Tests for acme.sig.Signature.""" - - def setUp(self): - self.msg = b'message' - self.sig = (b'IC\xd8*\xe7\x14\x9e\x19S\xb7\xcf\xec3\x12\xe2\x8a\x03' - b'\x98u\xff\xf0\x94\xe2\xd7<\x8f\xa8\xed\xa4KN\xc3\xaa' - b'\xb9X\xc3w\xaa\xc0_\xd0\x05$y>l#\x10<\x96\xd2\xcdr\xa3' - b'\x1b\xa1\xf5!f\xef\xc64\xb6\x13') - self.nonce = b'\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9' - - self.alg = jose.RS256 - self.jwk = jose.JWKRSA(key=KEY.public_key()) - - b64sig = ('SUPYKucUnhlTt8_sMxLiigOYdf_wlOLXPI-o7aRLTsOquVjDd6r' - 'AX9AFJHk-bCMQPJbSzXKjG6H1IWbvxjS2Ew') - b64nonce = '7Nbyb1lI6xPVI3Hg3aKSqQ' - self.jsig_to = { - 'nonce': b64nonce, - 'alg': self.alg, - 'jwk': self.jwk, - 'sig': b64sig, - } - - self.jsig_from = { - 'nonce': b64nonce, - 'alg': self.alg.to_partial_json(), - 'jwk': self.jwk.to_partial_json(), - 'sig': b64sig, - } - - from acme.other import Signature - self.signature = Signature( - alg=self.alg, sig=self.sig, nonce=self.nonce, jwk=self.jwk) - - def test_attributes(self): - self.assertEqual(self.signature.nonce, self.nonce) - self.assertEqual(self.signature.alg, self.alg) - self.assertEqual(self.signature.sig, self.sig) - self.assertEqual(self.signature.jwk, self.jwk) - - def test_verify_good_succeeds(self): - self.assertTrue(self.signature.verify(self.msg)) - - def test_verify_bad_fails(self): - self.assertFalse(self.signature.verify(self.msg + b'x')) - - @classmethod - def _from_msg(cls, *args, **kwargs): - from acme.other import Signature - return Signature.from_msg(*args, **kwargs) - - def test_create_from_msg(self): - signature = self._from_msg(self.msg, KEY, self.nonce) - self.assertEqual(self.signature, signature) - - def test_create_from_msg_random_nonce(self): - signature = self._from_msg(self.msg, KEY) - self.assertEqual(signature.alg, self.alg) - self.assertEqual(signature.jwk, self.jwk) - self.assertTrue(signature.verify(self.msg)) - - def test_to_partial_json(self): - self.assertEqual(self.signature.to_partial_json(), self.jsig_to) - - def test_from_json(self): - from acme.other import Signature - self.assertEqual( - self.signature, Signature.from_json(self.jsig_from)) - - def test_from_json_non_schema_errors(self): - from acme.other import Signature - jwk = self.jwk.to_partial_json() - self.assertRaises( - jose.DeserializationError, Signature.from_json, { - 'alg': 'RS256', 'sig': 'x', 'nonce': '', 'jwk': jwk}) - self.assertRaises( - jose.DeserializationError, Signature.from_json, { - 'alg': 'RS256', 'sig': '', 'nonce': 'x', 'jwk': jwk}) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover From ec1b14e388c3eea12bd6d258d4e332423d894a5b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 7 Mar 2016 18:47:23 -0800 Subject: [PATCH 07/23] Whatsa DV challenge --- acme/acme/challenges.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 0b15e7fb4..280bc8308 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -35,10 +35,6 @@ class Challenge(jose.TypedJSONObjectWithFields): return UnrecognizedChallenge.from_json(jobj) -class DVChallenge(Challenge): # pylint: disable=abstract-method - """Domain validation challenges.""" - - class ChallengeResponse(jose.TypedJSONObjectWithFields): # _fields_to_partial_json | pylint: disable=abstract-method """ACME challenge response.""" @@ -73,8 +69,8 @@ class UnrecognizedChallenge(Challenge): return cls(jobj) -class _TokenDVChallenge(DVChallenge): - """DV Challenge with token. +class _TokenChallenge(Challenge): + """Challenge with token. :ivar bytes token: @@ -144,7 +140,7 @@ class KeyAuthorizationChallengeResponse(ChallengeResponse): return True -class KeyAuthorizationChallenge(_TokenDVChallenge): +class KeyAuthorizationChallenge(_TokenChallenge): # pylint: disable=abstract-class-little-used,too-many-ancestors """Challenge based on Key Authorization. @@ -456,7 +452,7 @@ class TLSSNI01(KeyAuthorizationChallenge): @Challenge.register # pylint: disable=too-many-ancestors -class DNS(_TokenDVChallenge): +class DNS(_TokenChallenge): """ACME "dns" challenge.""" typ = "dns" From b9496733f65c1e84bff2cb477753fbd62b2f508d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 9 Mar 2016 15:29:14 -0800 Subject: [PATCH 08/23] Plugin doc cleanups --- docs/using.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index b2251d948..0e67c0271 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -71,11 +71,11 @@ Plugin Auth Inst Notes =========== ==== ==== =============================================================== apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on Debian-based distributions with ``libaugeas0`` 1.0+. -standalone_ Y N Uses a "standalone" webserver to obtain a cert. This is useful - on systems with no webserver, or when direct integration with - the local webserver is not supported or not desired. webroot_ Y N Obtains a cert by writing to the webroot directory of an already running webserver. +standalone_ Y N Uses a "standalone" webserver to obtain a cert. Requires + port 80 or 443 to be available. This is useful on systems + with no webserver, or when direct integration with the local webserver is not supported or not desired. manual_ Y N Helps you obtain a cert by giving you instructions to perform domain validation yourself. nginx_ Y Y Very experimental and not included in letsencrypt-auto_. @@ -87,15 +87,16 @@ There are also a number of third-party plugins for the client, provided by other Plugin Auth Inst Notes =========== ==== ==== =============================================================== plesk_ Y Y Integration with the Plesk web hosting tool - https://github.com/plesk/letsencrypt-plesk haproxy_ Y Y Inegration with the HAProxy load balancer - https://code.greenhost.net/open/letsencrypt-haproxy s3front_ Y Y Integration with Amazon CloudFront distribution of S3 buckets - https://github.com/dlapiduz/letsencrypt-s3front gandi_ Y Y Integration with Gandi's hosting products and API - https://github.com/Gandi/letsencrypt-gandi =========== ==== ==== =============================================================== +.. _plesk: https://github.com/plesk/letsencrypt-plesk +.. _haproxy: https://code.greenhost.net/open/letsencrypt-haproxy +.. _s3front: https://github.com/dlapiduz/letsencrypt-s3front +.. _gandi: https://github.com/Gandi/letsencrypt-gandi + Future plugins for IMAP servers, SMTP servers, IRC servers, etc, are likely to be installers but not authenticators. From 6ba5f175ae8f12d3dcab832717900ac7bcd0e71d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 9 Mar 2016 18:30:38 -0800 Subject: [PATCH 09/23] Prevent example command from overflowing margins --- docs/using.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 0e67c0271..5c7137895 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -127,7 +127,8 @@ potentially be a separate directory for each domain. When requested a certificate for multiple domains, each domain will use the most recently specified ``--webroot-path``. So, for instance, -``letsencrypt certonly --webroot -w /var/www/example/ -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net`` +``letsencrypt certonly --webroot -w /var/www/example/ -d www.example.com -d`` +``example.com -w /var/www/eg -d other.example.net -d m.other.example.net`` would obtain a single certificate for all of those names, using the ``/var/www/example`` webroot directory for the first two, and From e203a6121cdef16ce3b3cfb0e73e31823c615f38 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 9 Mar 2016 18:38:03 -0800 Subject: [PATCH 10/23] weird spacing fix --- docs/using.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 25046f0bb..2d389baf6 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -75,7 +75,8 @@ webroot_ Y N Obtains a cert by writing to the webroot directory of an already running webserver. standalone_ Y N Uses a "standalone" webserver to obtain a cert. Requires port 80 or 443 to be available. This is useful on systems - with no webserver, or when direct integration with the local webserver is not supported or not desired. + with no webserver, or when direct integration with the local + webserver is not supported or not desired. manual_ Y N Helps you obtain a cert by giving you instructions to perform domain validation yourself. nginx_ Y Y Very experimental and not included in letsencrypt-auto_. From 14f595d48ef4ad27560bb2f612b2709b82f57837 Mon Sep 17 00:00:00 2001 From: Wang Yu Date: Sat, 5 Mar 2016 20:33:02 +0100 Subject: [PATCH 11/23] webroot configuration text--fix format --- docs/using.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 04b239958..488afa2e0 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -126,7 +126,9 @@ potentially be a separate directory for each domain. When requested a certificate for multiple domains, each domain will use the most recently specified ``--webroot-path``. So, for instance, -``letsencrypt certonly --webroot -w /var/www/example/ -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net`` +:: + + letsencrypt certonly --webroot -w /var/www/example/ -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net would obtain a single certificate for all of those names, using the ``/var/www/example`` webroot directory for the first two, and From 34480f9d0f45f0aa1d9e282b894d9a886b05b67e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 10 Mar 2016 13:27:47 -0800 Subject: [PATCH 12/23] Revert "Remove SIGFILEBALL after creating sig" This reverts commit cf12f8702761a14ce55d89a2f75c23a996032c83. --- tools/offline-sigrequest.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/offline-sigrequest.sh b/tools/offline-sigrequest.sh index 08a5c4c05..7706796ef 100755 --- a/tools/offline-sigrequest.sh +++ b/tools/offline-sigrequest.sh @@ -42,7 +42,6 @@ function oncesigned { # $1 <-- INPFILE ; $2 <--SIGFILE echo `file $2` exit 1 fi - rm $SIGFILEBALL } HERE=`dirname $0` From 4a17294654a4157cdfb87abb4bd2ab55b0af111a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 10 Mar 2016 13:35:06 -0800 Subject: [PATCH 13/23] Remove sigfileball and add it to gitignore --- .gitignore | 1 + letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64 | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64 diff --git a/.gitignore b/.gitignore index 38c95986c..8118edfd4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ dist*/ /.tox/ /releases/ letsencrypt.log +letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64 # coverage .coverage diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64 b/letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64 deleted file mode 100644 index 037f2f020..000000000 --- a/letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64 +++ /dev/null @@ -1,6 +0,0 @@ -XQAAAAT//////////wApLArrUzOk5bRHUk0UvMS4xjyZkm3U3qhnKvMbEan7rVeK6yBlbwGeeWFn -Sw4XT1raGAMNq7cwyJvT7ql93Df7TpuRnxNSbPx7q52GojYyb5Oj1IQ2Y22Mvq41Q4K3kCZcVv+1 -YVKW3OazUn+wCnaoGhDdMFmH0EKbEPSGibba6HJqUoFosaDE2hRZmjqYR/VwwPCtW820L0Qz9PZ7 -DEAZ5VdMmj1+u+bYjDEcZD5+DyWKoLWci8tBXcPGiSvPDdZax/IWmR0GGUOd13gC7uX/HM2dHgbM -Izh7Y3PPNEzM8Fu2wdXLoMCaYrQcrPAdKhsnyMCDbjxCVbD9LkS17xCq4LUMkcz/fMu3/CRSMMZ7 -gnn//jNQAA== From 0aed0e90f1a1a8e0ca67b83e7187ac44966b593e Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 11 Mar 2016 17:07:13 -0800 Subject: [PATCH 14/23] don't include files that have multiple vhosts --- letsencrypt-apache/letsencrypt_apache/configurator.py | 1 + letsencrypt-apache/letsencrypt_apache/display_ops.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index 07c145bfc..3a679fa7e 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -545,6 +545,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): paths = self.aug.match( ("/files%s//*[label()=~regexp('%s')]" % (vhost_path, parser.case_i("VirtualHost")))) + paths = [path for path in paths if os.path.basename(path) == "VirtualHost"] for path in paths: new_vhost = self._create_vhost(path) realpath = os.path.realpath(new_vhost.filep) diff --git a/letsencrypt-apache/letsencrypt_apache/display_ops.py b/letsencrypt-apache/letsencrypt_apache/display_ops.py index 6a2308d73..bd3aa524d 100644 --- a/letsencrypt-apache/letsencrypt_apache/display_ops.py +++ b/letsencrypt-apache/letsencrypt_apache/display_ops.py @@ -83,7 +83,8 @@ def _vhost_menu(domain, vhosts): code, tag = zope.component.getUtility(interfaces.IDisplay).menu( "We were unable to find a vhost with a ServerName " "or Address of {0}.{1}Which virtual host would you " - "like to choose?".format(domain, os.linesep), + "like to choose?\n(note: conf files with multiple " + "vhosts are not currently supported)".format(domain, os.linesep), choices, help_label="More Info", ok_label="Select") except errors.MissingCommandlineFlag as e: msg = ("Failed to run Apache plugin non-interactively{1}{0}{1}" From cb7bd5a8e549540089e723e5665d30bb04dd393e Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 14 Mar 2016 16:21:27 -0700 Subject: [PATCH 15/23] Don't suggest --duplicate; it's likely to confuse people --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 51c8c55b8..8e545a5de 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -360,7 +360,7 @@ def _handle_subset_cert_request(config, domains, cert): br=os.linesep) if config.expand or config.renew_by_default or zope.component.getUtility( interfaces.IDisplay).yesno(question, "Expand", "Cancel", - cli_flag="--expand (or in some cases, --duplicate)"): + cli_flag="--expand"): return "renew", cert else: reporter_util = zope.component.getUtility(interfaces.IReporter) From 3fb990ac9bc4b71c262d97c153452e8d9a225a24 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 14 Mar 2016 17:19:22 -0700 Subject: [PATCH 16/23] fixes #2661 --- letsencrypt/le_util.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index c8a9d24c2..a92ce6e89 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -6,6 +6,7 @@ import logging import os import platform import re +import six import socket import stat import subprocess @@ -310,10 +311,13 @@ def enforce_domain_sanity(domain): # Unicode try: domain = domain.encode('ascii').lower() - except UnicodeDecodeError: - raise errors.ConfigurationError( - "Internationalized domain names are not presently supported: {0}" - .format(domain)) + except UnicodeError: + error_fmt = ("Internationalized domain names " + "are not presently supported: {0}") + if isinstance(domain, six.text_type): + raise errors.ConfigurationError(unicode(error_fmt).format(domain)) + else: + raise errors.ConfigurationError(error_fmt.format(domain)) # Remove trailing dot domain = domain[:-1] if domain.endswith('.') else domain From 1ff4f4c9ddeac41d38b3f145dd0138771d64573e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 14 Mar 2016 17:43:33 -0700 Subject: [PATCH 17/23] add tests for unicode {en,de}coding error --- letsencrypt/tests/le_util_test.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 191b70801..0f9464c6f 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -323,5 +323,21 @@ class AddDeprecatedArgumentTest(unittest.TestCase): self.assertTrue("--old-option" not in stdout.getvalue()) +class EnforceDomainSanityTest(unittest.TestCase): + """Test enforce_domain_sanity.""" + + def _call(self, domain): + from letsencrypt.le_util import enforce_domain_sanity + return enforce_domain_sanity(domain) + + def test_nonascii_str(self): + self.assertRaises(errors.ConfigurationError, self._call, + u"eichh\u00f6rnchen.example.com".encode("utf-8")) + + def test_nonascii_unicode(self): + self.assertRaises(errors.ConfigurationError, self._call, + u"eichh\u00f6rnchen.example.com") + + if __name__ == "__main__": unittest.main() # pragma: no cover From f354607104e6ffcad6ffde09c7adbf2bfbd41227 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 14 Mar 2016 18:02:28 -0700 Subject: [PATCH 18/23] logic flip --- letsencrypt/le_util.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index a92ce6e89..cb1c61074 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -312,12 +312,12 @@ def enforce_domain_sanity(domain): try: domain = domain.encode('ascii').lower() except UnicodeError: - error_fmt = ("Internationalized domain names " - "are not presently supported: {0}") + error_fmt = (u"Internationalized domain names " + "are not presently supported: {0}") if isinstance(domain, six.text_type): - raise errors.ConfigurationError(unicode(error_fmt).format(domain)) - else: raise errors.ConfigurationError(error_fmt.format(domain)) + else: + raise errors.ConfigurationError(str(error_fmt).format(domain)) # Remove trailing dot domain = domain[:-1] if domain.endswith('.') else domain From b19c74be327c03fdb2810fc492fbbc8a6e7ec71b Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 14 Mar 2016 18:44:29 -0700 Subject: [PATCH 19/23] Address review comments --- letsencrypt/cli.py | 7 +++---- letsencrypt/main.py | 11 ++++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index b76311777..c4d127718 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1,4 +1,4 @@ -"""Let's Encrypt command CLI argument processing.""" +"""Let's Encrypt command line argument & config processing.""" # pylint: disable=too-many-lines from __future__ import print_function import argparse @@ -634,10 +634,9 @@ class HelpfulArgumentParser(object): from letsencrypt import main self.VERBS = main.VERBS # List of topics for which additional help can be provided - HELP_TOPICS = ["all", "security", - "paths", "automation", "testing"] + list(six.iterkeys(self.VERBS)) + HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS) - plugin_names = list(six.iterkeys(plugins)) + plugin_names = list(plugins) self.help_topics = HELP_TOPICS + plugin_names + [None] usage, short_usage = usage_strings(plugins) self.parser = configargparse.ArgParser( diff --git a/letsencrypt/main.py b/letsencrypt/main.py index 56725d300..d2d2c55ac 100644 --- a/letsencrypt/main.py +++ b/letsencrypt/main.py @@ -2,8 +2,13 @@ from __future__ import print_function import atexit import functools +import logging.handlers import os import sys +import time +import traceback + +import OpenSSL import zope.component import letsencrypt @@ -22,15 +27,11 @@ from letsencrypt import log from letsencrypt import reporter from letsencrypt import storage +from acme import jose from letsencrypt.cli import choose_configurator_plugins, _renewal_conf_files, should_renew from letsencrypt.display import util as display_util, ops as display_ops from letsencrypt.plugins import disco as plugins_disco -import traceback -import logging.handlers -import time -from acme import jose -import OpenSSL logger = logging.getLogger(__name__) From dbb5b1731421e569ff2abab005bdba522212fbb9 Mon Sep 17 00:00:00 2001 From: YourDaddyIsHere Date: Sun, 13 Mar 2016 00:19:12 +0100 Subject: [PATCH 20/23] Update README.rst --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index 522400a1f..874b0c450 100644 --- a/README.rst +++ b/README.rst @@ -90,6 +90,11 @@ IRC Channel: #letsencrypt on `Freenode`_ Community: https://community.letsencrypt.org +ACME spec: http://ietf-wg-acme.github.io/acme/ + +ACME working area in github: https://github.com/ietf-wg-acme/acme + + Mailing list: `client-dev`_ (to subscribe without a Google account, send an email to client-dev+subscribe@letsencrypt.org) From 55eeb655bbcc55520b5a415d68e812d7d9166a5a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 14 Mar 2016 20:11:31 -0700 Subject: [PATCH 21/23] Moved VERBS back to cli.py --- letsencrypt/cli.py | 8 +++++-- letsencrypt/main.py | 9 -------- letsencrypt/tests/cli_test.py | 39 +++++------------------------------ 3 files changed, 11 insertions(+), 45 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 85a0d1d8a..662e1a94b 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -629,9 +629,13 @@ class HelpfulArgumentParser(object): """ def __init__(self, args, plugins, detect_defaults=False): - from letsencrypt import main - self.VERBS = main.VERBS + self.VERBS = {"auth": main.obtain_cert, "certonly": main.obtain_cert, + "config_changes": main.config_changes, "run": main.run, + "install": main.install, "plugins": main.plugins_cmd, + "renew": renew, "revoke": main.revoke, + "rollback": main.rollback, "everything": main.run} + # List of topics for which additional help can be provided HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS) diff --git a/letsencrypt/main.py b/letsencrypt/main.py index 19636b93e..264f7625e 100644 --- a/letsencrypt/main.py +++ b/letsencrypt/main.py @@ -701,15 +701,6 @@ def main(cli_args=sys.argv[1:]): return config.func(config, plugins) -# Maps verbs/subcommands to the functions that implement them -# In principle this should live in cli.HelpfulArgumentParser, but -# due to issues with import cycles and testing, it lives here -VERBS = {"auth": obtain_cert, "certonly": obtain_cert, - "config_changes": config_changes, "everything": run, - "install": install, "plugins": plugins_cmd, "renew": cli.renew, - "revoke": revoke, "rollback": rollback, "run": run} - - if __name__ == "__main__": err_string = main() if err_string: diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index c5865206d..7b901d410 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -79,7 +79,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods return ret, None, stderr, client def test_no_flags(self): - with MockedVerb("run") as mock_run: + with mock.patch('letsencrypt.main.run') as mock_run: self._call([]) self.assertEqual(1, mock_run.call_count) @@ -190,7 +190,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods chain = 'chain' fullchain = 'fullchain' - with MockedVerb('install') as mock_install: + with mock.patch('letsencrypt.main.install') as mock_install: self._call(['install', '--cert-path', cert, '--key-path', 'key', '--chain-path', 'chain', '--fullchain-path', 'fullchain']) @@ -248,7 +248,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods unused_config, auth, unused_installer = mock_init.call_args[0] self.assertTrue(isinstance(auth, manual.Authenticator)) - with MockedVerb("certonly") as mock_certonly: + with mock.patch('letsencrypt.main.obtain_cert') as mock_certonly: self._call(["auth", "--standalone"]) self.assertEqual(1, mock_certonly.call_count) @@ -321,7 +321,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods chain = 'chain' fullchain = 'fullchain' - with MockedVerb('certonly') as mock_obtaincert: + with mock.patch('letsencrypt.main.obtain_cert') as mock_obtaincert: self._call(['certonly', '--cert-path', cert, '--key-path', 'key', '--chain-path', 'chain', '--fullchain-path', 'fullchain']) @@ -900,7 +900,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertEqual(contents, test_contents) def test_agree_dev_preview_config(self): - with MockedVerb('run') as mocked_run: + with mock.patch('letsencrypt.main.run') as mocked_run: self._call(['-c', test_util.vector_path('cli.ini')]) self.assertTrue(mocked_run.called) @@ -1010,34 +1010,5 @@ class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): self.assertEqual(result, (None, None)) -class MockedVerb(object): - """Simple class that can be used for mocking out verbs/subcommands. - - Storing a dictionary of verbs and the functions that implement them - in letsencrypt.cli makes mocking much more complicated. This class - can be used as a simple context manager for mocking out verbs in CLI - tests. For example: - - with MockedVerb("run") as mock_run: - self._call([]) - self.assertEqual(1, mock_run.call_count) - - """ - def __init__(self, verb_name): - self.verb_dict = main.VERBS - self.verb_func = None - self.verb_name = verb_name - - def __enter__(self): - self.verb_func = self.verb_dict[self.verb_name] - mocked_func = mock.MagicMock() - self.verb_dict[self.verb_name] = mocked_func - - return mocked_func - - def __exit__(self, unused_type, unused_value, unused_trace): - self.verb_dict[self.verb_name] = self.verb_func - - if __name__ == '__main__': unittest.main() # pragma: no cover From 68ca289e7f84b7579787751a22bc56216b4ec955 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 15 Mar 2016 12:07:46 -0700 Subject: [PATCH 22/23] change wording --- letsencrypt-apache/letsencrypt_apache/display_ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-apache/letsencrypt_apache/display_ops.py b/letsencrypt-apache/letsencrypt_apache/display_ops.py index bd3aa524d..4c01579cc 100644 --- a/letsencrypt-apache/letsencrypt_apache/display_ops.py +++ b/letsencrypt-apache/letsencrypt_apache/display_ops.py @@ -84,7 +84,7 @@ def _vhost_menu(domain, vhosts): "We were unable to find a vhost with a ServerName " "or Address of {0}.{1}Which virtual host would you " "like to choose?\n(note: conf files with multiple " - "vhosts are not currently supported)".format(domain, os.linesep), + "vhosts are not yet supported)".format(domain, os.linesep), choices, help_label="More Info", ok_label="Select") except errors.MissingCommandlineFlag as e: msg = ("Failed to run Apache plugin non-interactively{1}{0}{1}" From 1fbfbda33c2ac04e848ffcf5611e58ec68cad1cf Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 17 Mar 2016 15:43:40 -0700 Subject: [PATCH 23/23] Address review nits --- letsencrypt/main.py | 3 ++- letsencrypt/tests/display/util_test.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/letsencrypt/main.py b/letsencrypt/main.py index 264f7625e..d82122481 100644 --- a/letsencrypt/main.py +++ b/letsencrypt/main.py @@ -11,6 +11,8 @@ import traceback import OpenSSL import zope.component +from acme import jose + import letsencrypt from letsencrypt import account @@ -27,7 +29,6 @@ from letsencrypt import log from letsencrypt import reporter from letsencrypt import storage -from acme import jose from letsencrypt.cli import choose_configurator_plugins, _renewal_conf_files, should_renew from letsencrypt.display import util as display_util, ops as display_ops from letsencrypt.plugins import disco as plugins_disco diff --git a/letsencrypt/tests/display/util_test.py b/letsencrypt/tests/display/util_test.py index 3f8ee8bb5..a16eb544e 100644 --- a/letsencrypt/tests/display/util_test.py +++ b/letsencrypt/tests/display/util_test.py @@ -8,6 +8,7 @@ import letsencrypt.errors as errors from letsencrypt.display import util as display_util + CHOICES = [("First", "Description1"), ("Second", "Description2")] TAGS = ["tag1", "tag2", "tag3"] TAGS_CHOICES = [("1", "tag1"), ("2", "tag2"), ("3", "tag3")]