diff --git a/.travis.yml b/.travis.yml index b35f0ebbf..ca06f07d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -94,11 +94,13 @@ matrix: # Only build pushes to the master branch, PRs, and branches beginning with -# `test-`. This reduces the number of simultaneous Travis runs, which speeds -# turnaround time on review since there is a cap of 5 simultaneous runs. +# `test-` or of the form `digit(s).digit(s).x`. This reduces the number of +# simultaneous Travis runs, which speeds turnaround time on review since there +# is a cap of on the number of simultaneous runs. branches: only: - master + - /^\d+\.\d+\.x$/ - /^test-.*$/ # container-based infrastructure diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 9f9cc05b8..83b9b9edd 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -9,7 +9,6 @@ from cryptography.hazmat.primitives import hashes import OpenSSL import requests -from acme import dns_resolver from acme import errors from acme import crypto_util from acme import fields @@ -183,7 +182,7 @@ class KeyAuthorizationChallenge(_TokenChallenge): Subclasses must implement this method, but they are likely to return completely different data structures, depending on what's - necessary to complete the challenge. Interepretation of that + necessary to complete the challenge. Interpretation of that return value must be known to the caller. :param JWK account_key: @@ -214,36 +213,24 @@ class DNS01Response(KeyAuthorizationChallengeResponse): def simple_verify(self, chall, domain, account_public_key): """Simple verify. + This method no longer checks DNS records and is a simple wrapper + around `KeyAuthorizationChallengeResponse.verify`. + :param challenges.DNS01 chall: Corresponding challenge. :param unicode domain: Domain name being verified. :param JWK account_public_key: Public key for the key pair being authorized. - :returns: ``True`` iff validation with the TXT records resolved from a - DNS server is successful. + :return: ``True`` iff verification of the key authorization was + successful. :rtype: bool """ - if not self.verify(chall, account_public_key): + # pylint: disable=unused-argument + verified = self.verify(chall, account_public_key) + if not verified: logger.debug("Verification of key authorization in response failed") - return False - - validation_domain_name = chall.validation_domain_name(domain) - validation = chall.validation(account_public_key) - logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) - - try: - txt_records = dns_resolver.txt_records_for_name( - validation_domain_name) - except errors.DependencyError: - raise errors.DependencyError("Local validation for 'dns-01' " - "challenges requires 'dnspython'") - exists = validation in txt_records - if not exists: - logger.debug("Key authorization from response (%r) doesn't match " - "any DNS response in %r", self.key_authorization, - txt_records) - return exists + return verified @Challenge.register # pylint: disable=too-many-ancestors diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 5ac07abdd..49e790102 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -10,7 +10,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 test_util -from acme.dns_resolver import DNS_REQUIREMENT CERT = test_util.load_comparable_cert('cert.pem') KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem')) @@ -92,7 +91,6 @@ class DNS01ResponseTest(unittest.TestCase): from acme.challenges import DNS01 self.chall = DNS01(token=(b'x' * 16)) self.response = self.chall.response(KEY) - self.records_for_name_path = "acme.dns_resolver.txt_records_for_name" def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) @@ -105,45 +103,16 @@ class DNS01ResponseTest(unittest.TestCase): from acme.challenges import DNS01Response hash(DNS01Response.from_json(self.jmsg)) - def test_simple_verify_bad_key_authorization(self): + def test_simple_verify_failure(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) - self.response.simple_verify(self.chall, "local", key2.public_key()) + public_key = key2.public_key() + verified = self.response.simple_verify(self.chall, "local", public_key) + self.assertFalse(verified) - @mock.patch('acme.dns_resolver.DNS_AVAILABLE', False) - def test_simple_verify_without_dns(self): - self.assertRaises( - errors.DependencyError, self.response.simple_verify, - self.chall, 'local', KEY.public_key()) - - @test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT), - "optional dependency dnspython is not available") - def test_simple_verify_good_validation(self): # pragma: no cover - with mock.patch(self.records_for_name_path) as mock_resolver: - mock_resolver.return_value = [ - self.chall.validation(KEY.public_key())] - self.assertTrue(self.response.simple_verify( - self.chall, "local", KEY.public_key())) - mock_resolver.assert_called_once_with( - self.chall.validation_domain_name("local")) - - @test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT), - "optional dependency dnspython is not available") - def test_simple_verify_good_validation_multitxts(self): # pragma: no cover - with mock.patch(self.records_for_name_path) as mock_resolver: - mock_resolver.return_value = [ - "!", self.chall.validation(KEY.public_key())] - self.assertTrue(self.response.simple_verify( - self.chall, "local", KEY.public_key())) - mock_resolver.assert_called_once_with( - self.chall.validation_domain_name("local")) - - @test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT), - "optional dependency dnspython is not available") - def test_simple_verify_bad_validation(self): # pragma: no cover - with mock.patch(self.records_for_name_path) as mock_resolver: - mock_resolver.return_value = ["!"] - self.assertFalse(self.response.simple_verify( - self.chall, "local", KEY.public_key())) + def test_simple_verify_success(self): + public_key = KEY.public_key() + verified = self.response.simple_verify(self.chall, "local", public_key) + self.assertTrue(verified) class DNS01Test(unittest.TestCase): diff --git a/acme/acme/client.py b/acme/acme/client.py index 19f566b0d..0324967cf 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -674,8 +674,23 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes self._add_nonce(self.head(url)) return self._nonces.pop() - def post(self, url, obj, content_type=JOSE_CONTENT_TYPE, **kwargs): - """POST object wrapped in `.JWS` and check response.""" + def post(self, *args, **kwargs): + """POST object wrapped in `.JWS` and check response. + + If the server responded with a badNonce error, the request will + be retried once. + + """ + try: + return self._post_once(*args, **kwargs) + except messages.Error as error: + if error.code == 'badNonce': + logger.debug('Retrying request after error:\n%s', error) + return self._post_once(*args, **kwargs) + else: + raise + + def _post_once(self, url, obj, content_type=JOSE_CONTENT_TYPE, **kwargs): data = self._wrap_in_jws(obj, self._get_nonce(url)) kwargs.setdefault('headers', {'Content-Type': content_type}) response = self._send_request('POST', url, data=data, **kwargs) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 62555939c..179a8a08c 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -170,7 +170,7 @@ class ClientTest(unittest.TestCase): self.directory.new_authz, messages.NewAuthorization(identifier=self.identifier)) - def test_requets_challenges_custom_uri(self): + def test_request_challenges_custom_uri(self): self._prepare_response_for_request_challenges() self.client.request_challenges(self.identifier, 'URI') self.net.post.assert_called_once_with('URI', mock.ANY) @@ -388,7 +388,7 @@ class ClientTest(unittest.TestCase): errors.PollError, self.client.poll_and_request_issuance, csr, authzrs=(invalid_authzr,), mintime=mintime) - # exceeded max_attemps | TODO: move to a separate test + # exceeded max_attempts | TODO: move to a separate test self.assertRaises( errors.PollError, self.client.poll_and_request_issuance, csr, authzrs, mintime=mintime, max_attempts=2) @@ -642,7 +642,9 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): self.wrapped_obj = mock.MagicMock() self.content_type = mock.sentinel.content_type - self.all_nonces = [jose.b64encode(b'Nonce'), jose.b64encode(b'Nonce2')] + self.all_nonces = [ + jose.b64encode(b'Nonce'), + jose.b64encode(b'Nonce2'), jose.b64encode(b'Nonce3')] self.available_nonces = self.all_nonces[:] def send_request(*args, **kwargs): @@ -690,7 +692,7 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): self.net._wrap_in_jws.assert_called_once_with( self.obj, jose.b64decode(self.all_nonces.pop())) - assert not self.available_nonces + self.available_nonces = [] self.assertRaises(errors.MissingNonce, self.net.post, 'uri', self.obj, content_type=self.content_type) self.net._wrap_in_jws.assert_called_with( @@ -706,6 +708,35 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): self.assertRaises(errors.BadNonce, self.net.post, 'uri', self.obj, content_type=self.content_type) + def test_post_failed_retry(self): + check_response = mock.MagicMock() + check_response.side_effect = messages.Error.with_code('badNonce') + + # pylint: disable=protected-access + self.net._check_response = check_response + self.assertRaises(messages.Error, self.net.post, 'uri', + self.obj, content_type=self.content_type) + + def test_post_not_retried(self): + check_response = mock.MagicMock() + check_response.side_effect = [messages.Error.with_code('malformed'), + self.checked_response] + + # pylint: disable=protected-access + self.net._check_response = check_response + self.assertRaises(messages.Error, self.net.post, 'uri', + self.obj, content_type=self.content_type) + + def test_post_successful_retry(self): + check_response = mock.MagicMock() + check_response.side_effect = [messages.Error.with_code('badNonce'), + self.checked_response] + + # pylint: disable=protected-access + self.net._check_response = check_response + self.assertEqual(self.checked_response, self.net.post( + 'uri', self.obj, content_type=self.content_type)) + def test_head_get_post_error_passthrough(self): self.send_request.side_effect = requests.exceptions.RequestException for method in self.net.head, self.net.get: diff --git a/acme/acme/crypto_util_test.py b/acme/acme/crypto_util_test.py index bd93ae0e1..ebb4010a6 100644 --- a/acme/acme/crypto_util_test.py +++ b/acme/acme/crypto_util_test.py @@ -59,7 +59,7 @@ class SSLSocketAndProbeSNITest(unittest.TestCase): def test_probe_not_recognized_name(self): self.assertRaises(errors.Error, self._probe, b'bar') - # TODO: py33/py34 tox hangs forever on do_hendshake in second probe + # TODO: py33/py34 tox hangs forever on do_handshake in second probe #def probe_connection_error(self): # self._probe(b'foo') # #time.sleep(1) # TODO: avoid race conditions in other way diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py deleted file mode 100644 index 2677d92ad..000000000 --- a/acme/acme/dns_resolver.py +++ /dev/null @@ -1,45 +0,0 @@ -"""DNS Resolver for ACME client. -Required only for local validation of 'dns-01' challenges. -""" -import logging - -from acme import errors -from acme import util - -DNS_REQUIREMENT = 'dnspython>=1.12' - -try: - util.activate(DNS_REQUIREMENT) - # pragma: no cover - import dns.exception - import dns.resolver - DNS_AVAILABLE = True -except errors.DependencyError: # pragma: no cover - DNS_AVAILABLE = False - - -logger = logging.getLogger(__name__) - - -def txt_records_for_name(name): - """Resolve the name and return the TXT records. - - :param unicode name: Domain name being verified. - - :returns: A list of txt records, if empty the name could not be resolved - :rtype: list of unicode - - """ - if not DNS_AVAILABLE: - raise errors.DependencyError( - '{0} is required to use this function'.format(DNS_REQUIREMENT)) - try: - dns_response = dns.resolver.query(name, 'TXT') - except dns.resolver.NXDOMAIN as error: - return [] - except dns.exception.DNSException as error: - logger.error("Error resolving %s: %s", name, str(error)) - return [] - - return [txt_rec.decode("utf-8") for rdata in dns_response - for txt_rec in rdata.strings] diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py deleted file mode 100644 index 2e2edd0e7..000000000 --- a/acme/acme/dns_resolver_test.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Tests for acme.dns_resolver.""" -import unittest - -import mock -from six.moves import reload_module # pylint: disable=import-error - -from acme import errors -from acme import test_util -from acme.dns_resolver import DNS_REQUIREMENT - - -if test_util.requirement_available(DNS_REQUIREMENT): - import dns - - -def create_txt_response(name, txt_records): - """ - Returns an RRSet containing the 'txt_records' as the result of a DNS - query for 'name'. - - This takes advantage of the fact that an Answer object mostly behaves - like an RRset. - """ - return dns.rrset.from_text_list(name, 60, "IN", "TXT", txt_records) - - -class TxtRecordsForNameTest(unittest.TestCase): - """Tests for acme.dns_resolver.txt_records_for_name.""" - @classmethod - def _call(cls, *args, **kwargs): - from acme.dns_resolver import txt_records_for_name - return txt_records_for_name(*args, **kwargs) - - -@test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT), - "optional dependency dnspython is not available") -class TxtRecordsForNameWithDnsTest(TxtRecordsForNameTest): - """Tests for acme.dns_resolver.txt_records_for_name with dns.""" - @mock.patch("acme.dns_resolver.dns.resolver.query") - def test_txt_records_for_name_with_single_response(self, mock_dns): - mock_dns.return_value = create_txt_response('name', ['response']) - self.assertEqual(['response'], self._call('name')) - - @mock.patch("acme.dns_resolver.dns.resolver.query") - def test_txt_records_for_name_with_multiple_responses(self, mock_dns): - mock_dns.return_value = create_txt_response( - 'name', ['response1', 'response2']) - self.assertEqual(['response1', 'response2'], self._call('name')) - - @mock.patch("acme.dns_resolver.dns.resolver.query") - def test_txt_records_for_name_domain_not_found(self, mock_dns): - mock_dns.side_effect = dns.resolver.NXDOMAIN - self.assertEquals([], self._call('name')) - - @mock.patch("acme.dns_resolver.dns.resolver.query") - def test_txt_records_for_name_domain_other_error(self, mock_dns): - mock_dns.side_effect = dns.exception.DNSException - self.assertEquals([], self._call('name')) - - -class TxtRecordsForNameWithoutDnsTest(TxtRecordsForNameTest): - """Tests for acme.dns_resolver.txt_records_for_name without dns.""" - def setUp(self): - from acme import dns_resolver - dns_resolver.DNS_AVAILABLE = False - - def tearDown(self): - from acme import dns_resolver - reload_module(dns_resolver) - - def test_exception_raised(self): - self.assertRaises( - errors.DependencyError, self._call, "example.org") - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/acme/jose/__init__.py b/acme/acme/jose/__init__.py index f39c3beab..9116bc433 100644 --- a/acme/acme/jose/__init__.py +++ b/acme/acme/jose/__init__.py @@ -1,6 +1,6 @@ """Javascript Object Signing and Encryption (jose). -This package is a Python implementation of the stadards developed by +This package is a Python implementation of the standards developed by IETF `Javascript Object Signing and Encryption (Active WG)`_, in particular the following RFCs: diff --git a/acme/acme/jose/json_util.py b/acme/acme/jose/json_util.py index cc66d77ff..d474f4aac 100644 --- a/acme/acme/jose/json_util.py +++ b/acme/acme/jose/json_util.py @@ -60,7 +60,7 @@ class Field(object): @classmethod def _empty(cls, value): - """Is the provided value cosidered "empty" for this field? + """Is the provided value considered "empty" for this field? This is useful for subclasses that might want to override the definition of being empty, e.g. for some more exotic data types. diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py index 4d07229b3..5b6965c4d 100644 --- a/acme/acme/jose/jwk.py +++ b/acme/acme/jose/jwk.py @@ -111,7 +111,7 @@ class JWK(json_util.TypedJSONObjectWithFields): try: key = cls._load_cryptography_key(data, password, backend) except errors.Error as error: - logger.debug('Loading symmetric key, assymentric failed: %s', error) + logger.debug('Loading symmetric key, asymmetric failed: %s', error) return JWKOct(key=data) if cls.typ is not NotImplemented and not isinstance( diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index a0322968c..b3454f25b 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -26,7 +26,7 @@ class ErrorTest(unittest.TestCase): 'type': ERROR_PREFIX + 'malformed', } self.error_custom = Error(typ='custom', detail='bar') - self.jobj_cusom = {'type': 'custom', 'detail': 'bar'} + self.jobj_custom = {'type': 'custom', 'detail': 'bar'} def test_default_typ(self): from acme.messages import Error diff --git a/acme/acme/test_util.py b/acme/acme/test_util.py index ba968511f..0f5763682 100644 --- a/acme/acme/test_util.py +++ b/acme/acme/test_util.py @@ -11,9 +11,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization import OpenSSL -from acme import errors from acme import jose -from acme import util def vector_path(*names): @@ -78,20 +76,6 @@ def load_pyopenssl_private_key(*names): return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) -def requirement_available(requirement): - """Checks if requirement can be imported. - - :rtype: bool - :returns: ``True`` iff requirement can be imported - - """ - try: - util.activate(requirement) - except errors.DependencyError: # pragma: no cover - return False - return True # pragma: no cover - - def skip_unless(condition, reason): # pragma: no cover """Skip tests unless a condition holds. diff --git a/acme/acme/util.py b/acme/acme/util.py index ac445b271..1fff89a9e 100644 --- a/acme/acme/util.py +++ b/acme/acme/util.py @@ -1,25 +1,7 @@ """ACME utilities.""" -import pkg_resources import six -from acme import errors - def map_keys(dikt, func): """Map dictionary keys.""" return dict((func(key), value) for key, value in six.iteritems(dikt)) - - -def activate(requirement): - """Make requirement importable. - - :param str requirement: the distribution and version to activate - - :raises acme.errors.DependencyError: if cannot activate requirement - - """ - try: - for distro in pkg_resources.require(requirement): # pylint: disable=not-callable - distro.activate() - except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict): - raise errors.DependencyError('{0} is unavailable'.format(requirement)) diff --git a/acme/acme/util_test.py b/acme/acme/util_test.py index ba6465409..00aa8b02d 100644 --- a/acme/acme/util_test.py +++ b/acme/acme/util_test.py @@ -1,8 +1,6 @@ """Tests for acme.util.""" import unittest -from acme import errors - class MapKeysTest(unittest.TestCase): """Tests for acme.util.map_keys.""" @@ -14,21 +12,5 @@ class MapKeysTest(unittest.TestCase): self.assertEqual({2: 2, 4: 4}, map_keys({1: 2, 3: 4}, lambda x: x + 1)) -class ActivateTest(unittest.TestCase): - """Tests for acme.util.activate.""" - - @classmethod - def _call(cls, *args, **kwargs): - from acme.util import activate - return activate(*args, **kwargs) - - def test_failure(self): - self.assertRaises(errors.DependencyError, self._call, 'acme>99.0.0') - - def test_success(self): - self._call('acme') - import acme as unused_acme - - if __name__ == '__main__': unittest.main() # pragma: no cover diff --git a/acme/setup.py b/acme/setup.py index 37b71368c..d1e91e5ec 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.11.0.dev0' +version = '0.12.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ @@ -33,11 +33,6 @@ if sys.version_info < (2, 7): else: install_requires.append('mock') -# dnspython 1.12 is required to support both Python 2 and Python 3. -dns_extras = [ - 'dnspython>=1.12', -] - dev_extras = [ 'nose', 'tox', @@ -77,7 +72,6 @@ setup( include_package_data=True, install_requires=install_requires, extras_require={ - 'dns': dns_extras, 'dev': dev_extras, 'docs': docs_extras, }, diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index bc7cffa72..ed2482ec1 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -472,7 +472,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "\n\nUnfortunately mod_macro is not yet supported".format( "\n ".join(vhost_macro)), force_interactive=True) - return all_names + return util.get_filtered_names(all_names) def get_name_from_ip(self, addr): # pylint: disable=no-self-use """Returns a reverse dns name if available. diff --git a/certbot-apache/certbot_apache/obj.py b/certbot-apache/certbot_apache/obj.py index c71443e92..b29b0e0ee 100644 --- a/certbot-apache/certbot_apache/obj.py +++ b/certbot-apache/certbot_apache/obj.py @@ -8,7 +8,7 @@ class Addr(common.Addr): """Represents an Apache address.""" def __eq__(self, other): - """This is defined as equalivalent within Apache. + """This is defined as equivalent within Apache. ip_addr:* == ip_addr diff --git a/certbot-apache/certbot_apache/tests/apache-conf-files/passing/comment-continuations-2050.conf b/certbot-apache/certbot_apache/tests/apache-conf-files/passing/comment-continuations-2050.conf index 48b344d8a..4c3fa2af1 100644 --- a/certbot-apache/certbot_apache/tests/apache-conf-files/passing/comment-continuations-2050.conf +++ b/certbot-apache/certbot_apache/tests/apache-conf-files/passing/comment-continuations-2050.conf @@ -263,7 +263,7 @@ # # Set the following policy settings here and they will be propagated to the 30 rules # file (modsecurity_crs_30_http_policy.conf) by using macro expansion. -# If you run into false positves, you can adjust the settings here. +# If you run into false positives, you can adjust the settings here. # #SecAction \ "id:'900012', \ @@ -349,7 +349,7 @@ # -# -- [[ Check UTF enconding ]] ----------------------------------------------------------- +# -- [[ Check UTF encoding ]] ----------------------------------------------------------- # # We only want to apply this check if UTF-8 encoding is actually used by the site, otherwise # it will result in false positives. diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 892126398..101069a51 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -126,8 +126,8 @@ class MultipleVhostsTest(util.ApacheTest): mock_getutility.notification = mock.MagicMock(return_value=True) names = self.config.get_all_names() self.assertEqual(names, set( - ["certbot.demo", "ocspvhost.com", "encryption-example.demo", - "ip-172-30-0-17", "*.blue.purple.com"])) + ["certbot.demo", "ocspvhost.com", "encryption-example.demo"] + )) @certbot_util.patch_get_utility() @mock.patch("certbot_apache.configurator.socket.gethostbyaddr") @@ -146,7 +146,8 @@ class MultipleVhostsTest(util.ApacheTest): self.config.vhosts.append(vhost) names = self.config.get_all_names() - self.assertEqual(len(names), 7) + # Names get filtered, only 5 are returned + self.assertEqual(len(names), 5) self.assertTrue("zombo.com" in names) self.assertTrue("google.com" in names) self.assertTrue("certbot.demo" in names) diff --git a/certbot-apache/certbot_apache/tests/parser_test.py b/certbot-apache/certbot_apache/tests/parser_test.py index 79c6a6753..d4f6e8d17 100644 --- a/certbot-apache/certbot_apache/tests/parser_test.py +++ b/certbot-apache/certbot_apache/tests/parser_test.py @@ -178,7 +178,7 @@ class ParserInitTest(util.ApacheTest): shutil.rmtree(self.work_dir) @mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg") - def test_unparsable(self, mock_cfg): + def test_unparseable(self, mock_cfg): from certbot_apache.parser import ApacheParser mock_cfg.return_value = ('Define: TEST') self.assertRaises( diff --git a/certbot-apache/certbot_apache/tls_sni_01.py b/certbot-apache/certbot_apache/tls_sni_01.py index 18179d080..d9e294119 100644 --- a/certbot-apache/certbot_apache/tls_sni_01.py +++ b/certbot-apache/certbot_apache/tls_sni_01.py @@ -177,7 +177,7 @@ class ApacheTlsSni01(common.TLSSNI01): ips = " ".join(str(i) for i in ip_addrs) document_root = os.path.join( self.configurator.config.work_dir, "tls_sni_01_page/") - # TODO: Python docs is not clear how mutliline string literal + # TODO: Python docs is not clear how multiline string literal # newlines are parsed on different platforms. At least on # Linux (Debian sid), when source file uses CRLF, Python still # parses it as "\n"... c.f.: diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index d50a414a1..87ea1a281 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.11.0.dev0' +version = '0.12.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index b22916997..90b8157fe 100755 --- a/certbot-auto +++ b/certbot-auto @@ -23,7 +23,7 @@ if [ -z "$VENV_PATH" ]; then VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" fi VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.10.1" +LE_AUTO_VERSION="0.11.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -38,8 +38,9 @@ Help for certbot itself cannot be provided until it is installed. -n, --non-interactive, --noninteractive run without asking for user input --no-self-upgrade do not download updates --os-packages-only install OS dependencies and exit - -q, --quiet provide only update/error output -v, --verbose provide more output + -q, --quiet provide only update/error output; + implies --non-interactive All arguments are accepted and forwarded to the Certbot client when run." @@ -84,6 +85,11 @@ if [ $BASENAME = "letsencrypt-auto" ]; then HELP=0 fi +# Set ASSUME_YES to 1 if QUIET (i.e. --quiet implies --non-interactive) +if [ "$QUIET" = 1 ]; then + ASSUME_YES=1 +fi + # Support for busybox and others where there is no "command", # but "which" instead if command -v command > /dev/null 2>&1 ; then @@ -99,7 +105,7 @@ fi # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but # this script *can* be run as root (not recommended), or fall back to using -# `su`. Auto-detection can be overrided by explicitly setting the +# `su`. Auto-detection can be overridden by explicitly setting the # environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below. # Because the parameters in `su -c` has to be a string, @@ -207,7 +213,11 @@ BootstrapDebCommon() { # # - Debian 6.0.10 "squeeze" (x64) - $SUDO apt-get update || echo apt-get update hit problems but continuing anyway... + if [ "$QUIET" = 1 ]; then + QUIET_FLAG='-qq' + fi + + $SUDO apt-get $QUIET_FLAG update || echo apt-get update hit problems but continuing anyway... # virtualenv binary can be found in different packages depending on # distro version (#346) @@ -215,74 +225,74 @@ BootstrapDebCommon() { virtualenv= # virtual env is known to apt and is installable if apt-cache show virtualenv > /dev/null 2>&1 ; then - if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then - virtualenv="virtualenv" - fi + if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then + virtualenv="virtualenv" + fi fi if apt-cache show python-virtualenv > /dev/null 2>&1; then - virtualenv="$virtualenv python-virtualenv" + virtualenv="$virtualenv python-virtualenv" fi augeas_pkg="libaugeas0 augeas-lenses" AUGVERSION=`LC_ALL=C apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2` if [ "$ASSUME_YES" = 1 ]; then - YES_FLAG="-y" + YES_FLAG="-y" fi AddBackportRepo() { - # ARGS: - BACKPORT_NAME="$1" - BACKPORT_SOURCELINE="$2" - echo "To use the Apache Certbot plugin, augeas needs to be installed from $BACKPORT_NAME." - if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then - # This can theoretically error if sources.list.d is empty, but in that case we don't care. - if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then - if [ "$ASSUME_YES" = 1 ]; then - /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." - sleep 1s - /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." - sleep 1s - /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." - sleep 1s - add_backports=1 - else - read -p "Would you like to enable the $BACKPORT_NAME repository [Y/n]? " response - case $response in - [yY][eE][sS]|[yY]|"") - add_backports=1;; - *) - add_backports=0;; - esac - fi - if [ "$add_backports" = 1 ]; then - $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" - $SUDO apt-get update - fi - fi - fi - if [ "$add_backports" != 0 ]; then - $SUDO apt-get install $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg - augeas_pkg= + # ARGS: + BACKPORT_NAME="$1" + BACKPORT_SOURCELINE="$2" + echo "To use the Apache Certbot plugin, augeas needs to be installed from $BACKPORT_NAME." + if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then + # This can theoretically error if sources.list.d is empty, but in that case we don't care. + if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then + if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." + sleep 1s + add_backports=1 + else + read -p "Would you like to enable the $BACKPORT_NAME repository [Y/n]? " response + case $response in + [yY][eE][sS]|[yY]|"") + add_backports=1;; + *) + add_backports=0;; + esac + fi + if [ "$add_backports" = 1 ]; then + $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" + $SUDO apt-get $QUIET_FLAG update + fi fi + fi + if [ "$add_backports" != 0 ]; then + $SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg + augeas_pkg= + fi } if dpkg --compare-versions 1.0 gt "$AUGVERSION" ; then - if lsb_release -a | grep -q wheezy ; then - AddBackportRepo wheezy-backports "deb http://http.debian.net/debian wheezy-backports main" - elif lsb_release -a | grep -q precise ; then - # XXX add ARM case - AddBackportRepo precise-backports "deb http://archive.ubuntu.com/ubuntu precise-backports main restricted universe multiverse" - else - echo "No libaugeas0 version is available that's new enough to run the" - echo "Certbot apache plugin..." - fi - # XXX add a case for ubuntu PPAs + if lsb_release -a | grep -q wheezy ; then + AddBackportRepo wheezy-backports "deb http://http.debian.net/debian wheezy-backports main" + elif lsb_release -a | grep -q precise ; then + # XXX add ARM case + AddBackportRepo precise-backports "deb http://archive.ubuntu.com/ubuntu precise-backports main restricted universe multiverse" + else + echo "No libaugeas0 version is available that's new enough to run the" + echo "Certbot apache plugin..." + fi + # XXX add a case for ubuntu PPAs fi - $SUDO apt-get install $YES_FLAG --no-install-recommends \ + $SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \ python \ python-dev \ $virtualenv \ @@ -294,7 +304,6 @@ BootstrapDebCommon() { ca-certificates \ - if ! $EXISTS virtualenv > /dev/null ; then echo Failed to install a working \"virtualenv\" command, exiting exit 1 @@ -323,6 +332,9 @@ BootstrapRpmCommon() { if [ "$ASSUME_YES" = 1 ]; then yes_flag="-y" fi + if [ "$QUIET" = 1 ]; then + QUIET_FLAG='--quiet' + fi if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then echo "To use Certbot, packages from the EPEL repository need to be installed." @@ -331,14 +343,14 @@ BootstrapRpmCommon() { exit 1 fi if [ "$ASSUME_YES" = 1 ]; then - /bin/echo -n "Enabling the EPEL repository in 3 seconds..." - sleep 1s - /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." - sleep 1s - /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." - sleep 1s + /bin/echo -n "Enabling the EPEL repository in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." + sleep 1s fi - if ! $SUDO $tool install $yes_flag epel-release; then + if ! $SUDO $tool install $yes_flag $QUIET_FLAG epel-release; then echo "Could not enable EPEL. Aborting bootstrap!" exit 1 fi @@ -380,9 +392,9 @@ BootstrapRpmCommon() { " fi - if ! $SUDO $tool install $yes_flag $pkgs; then - echo "Could not install OS dependencies. Aborting bootstrap!" - exit 1 + if ! $SUDO $tool install $yes_flag $QUIET_FLAG $pkgs; then + echo "Could not install OS dependencies. Aborting bootstrap!" + exit 1 fi } @@ -394,7 +406,11 @@ BootstrapSuseCommon() { install_flags="-l" fi - $SUDO zypper $zypper_flags in $install_flags \ + if [ "$QUIET" = 1 ]; then + QUIET_FLAG='-qq' + fi + + $SUDO zypper $QUIET_FLAG $zypper_flags in $install_flags \ python \ python-devel \ python-virtualenv \ @@ -432,7 +448,11 @@ BootstrapArchCommon() { fi if [ "$missing" ]; then - $SUDO pacman -S --needed $missing $noconfirm + if [ "$QUIET" = 1]; then + $SUDO pacman -S --needed $missing $noconfirm > /dev/null + else + $SUDO pacman -S --needed $missing $noconfirm + fi fi } @@ -465,7 +485,11 @@ BootstrapGentooCommon() { } BootstrapFreeBsd() { - $SUDO pkg install -Ay \ + if [ "$QUIET" = 1 ]; then + QUIET_FLAG="--quiet" + fi + + $SUDO pkg install -Ay $QUIET_FLAG \ python \ py27-virtualenv \ augeas \ @@ -505,15 +529,15 @@ BootstrapMac() { fi if ! hash pip 2>/dev/null; then - echo "pip not installed" - echo "Installing pip..." - curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | python + echo "pip not installed" + echo "Installing pip..." + curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | python fi if ! hash virtualenv 2>/dev/null; then - echo "virtualenv not installed." - echo "Installing with pip..." - pip install virtualenv + echo "virtualenv not installed." + echo "Installing with pip..." + pip install virtualenv fi } @@ -523,26 +547,29 @@ BootstrapSmartOS() { } BootstrapMageiaCommon() { - if ! $SUDO urpmi --force \ - python \ - libpython-devel \ - python-virtualenv + if [ "$QUIET" = 1 ]; then + QUIET_FLAG='--quiet' + fi + + if ! $SUDO urpmi --force $QUIET_FLAG \ + python \ + libpython-devel \ + python-virtualenv then echo "Could not install Python dependencies. Aborting bootstrap!" exit 1 - fi + fi - if ! $SUDO urpmi --force \ - git \ - gcc \ - python-augeas \ - openssl \ - libopenssl-devel \ - libffi-devel \ - rootcerts + if ! $SUDO urpmi --force $QUIET_FLAG \ + git \ + gcc \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts then - echo "Could not install additional dependencies. Aborting bootstrap!" - exit 1 + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 fi } @@ -609,6 +636,11 @@ if [ "$1" = "--le-auto-phase2" ]; then # --version output ran through grep due to python-cryptography DeprecationWarnings # grep for both certbot and letsencrypt until certbot and shim packages have been released INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep "^certbot\|^letsencrypt" | cut -d " " -f 2) + if [ -z "$INSTALLED_VERSION" ]; then + echo "Error: couldn't get currently installed version for $VENV_BIN/letsencrypt: " 1>&2 + "$VENV_BIN/letsencrypt" --version + exit 1 + fi else INSTALLED_VERSION="none" fi @@ -801,18 +833,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.10.1 \ - --hash=sha256:1dd5124078bc44739065409f3be51765608a90994c83460578c2680c582c1026 \ - --hash=sha256:f51c2fb0a31646364abeb7fdd8cfc7c8a4e63b0641b14ab3ce1b2e3a8921a211 -certbot==0.10.1 \ - --hash=sha256:9a56fc76f726beeed2f5a08d690088377cd430907f8a38c50e2aa9a258ee1253 \ - --hash=sha256:e0d699adb3f8ca3e077a4db339de29ebb3f790fbc5f3f02e446e227ed40aa743 -certbot-apache==0.10.1 \ - --hash=sha256:1252fd7e435ba48484b0bd9b72535a9755b03d8f0440f164b9c1c560d96cadb8 \ - --hash=sha256:134f46690da55262125defa58aa74472eb4a1555c9ed83edb3c8667df5a561b5 -certbot-nginx==0.10.1 \ - --hash=sha256:afd15ed9e4f3076056b63916f272b7287084d871cb8136477d16b08f64d514f0 \ - --hash=sha256:da1b7ea4831ead3f9eb526ee11bf1bf197da0fea4defeeb7b1ce24c5d3f45b51 +acme==0.11.0 \ + --hash=sha256:9c084f9a62241a11231af63266f2f12ad696be590393a4ab4974276a47d63404 \ + --hash=sha256:1513ae74ee8424c739a953a552890830315669d95d105469d1cff5b7db9ae888 +certbot==0.11.0 \ + --hash=sha256:cc94d890a8697b3bdbdc555bcf4ba93955f64324bc256b2ea710fd053fc03b8a \ + --hash=sha256:0f91fee360f9ce5e0584d0954fa3123832435f77f465915389032a90ac0248b1 +certbot-apache==0.11.0 \ + --hash=sha256:9a01883ca7e1159cff2a6e36bf97b83793c899c62944335f6ac2f59f9b3e8b5d \ + --hash=sha256:2b67871e7ae8bbfa7a2779fcd6444f28847fbb7a347ef4bfdb19fb55a0d75673 +certbot-nginx==0.11.0 \ + --hash=sha256:cfae45a42560e39889eebd287437556084c421a9e07a2deb3cbc0aeef92d2dab \ + --hash=sha256:dbddffe47c8e8b2d1cf47fe393b434003270a45aec896f133492c855c77e6f08 UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -1024,7 +1056,7 @@ UNLIKELY_EOF fi else - # Phase 1: Upgrade certbot-auto if neceesary, then self-invoke. + # Phase 1: Upgrade certbot-auto if necessary, then self-invoke. # # Each phase checks the version of only the thing it is responsible for # upgrading. Phase 1 checks the version of the latest release of diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 64170ca72..2e9e68daf 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -8,6 +8,7 @@ import zope.interface from certbot import configuration from certbot import errors as le_errors +from certbot import util as certbot_util from certbot_apache import configurator from certbot_apache import constants from certbot_compatibility_test import errors @@ -106,4 +107,7 @@ def _get_names(config): not util.IP_REGEX.match(words[1]) and words[1].find(".") != -1): all_names.add(words[1]) - return all_names, non_ip_names + return ( + certbot_util.get_filtered_names(all_names), + certbot_util.get_filtered_names(non_ip_names) + ) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index b5e023f36..71100bb27 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -147,7 +147,7 @@ def test_deploy_cert(plugin, temp_dir, domains): plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path, cert_path) plugin.save() # Needed by the Apache plugin except le_errors.Error as error: - logger.error("Plugin failed to deploy ceritificate for %s:", domain) + logger.error("Plugin failed to deploy certificate for %s:", domain) logger.exception(error) return False @@ -202,7 +202,7 @@ def test_enhancements(plugin, domains): success = False if success: - logger.info("Enhancments test succeeded") + logger.info("Enhancements test succeeded") return success diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/nginx.conf b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/nginx.conf index 22ad4c317..bc708783f 100644 --- a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/nginx.conf +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/nginx.conf @@ -38,7 +38,7 @@ http { ## Define a zone for limiting the number of simultaneous ## connections nginx accepts. 1m means 32000 simultaneous ## sessions. We need to define for each server the limit_conn - ## value refering to this or other zones. + ## value referring to this or other zones. ## ** This syntax requires nginx version >= ## ** 1.1.8. Cf. http://nginx.org/en/CHANGES. If using an older ## ** version then use the limit_zone directive below diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/win-utf b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/win-utf index ed8bc007a..d0b7116c8 100644 --- a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/win-utf +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/win-utf @@ -37,7 +37,7 @@ charset_map windows-1251 utf-8 { AA D084 ; # capital Ukrainian YE AB C2AB ; # left-pointing double angle quotation mark AC C2AC ; # not sign - AD C2AD ; # soft hypen + AD C2AD ; # soft hyphen AE C2AE ; # (R) AF D087 ; # capital Ukrainian YI diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/guide-to-nginx-ssl-spdy-hsts/nginx.conf b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/guide-to-nginx-ssl-spdy-hsts/nginx.conf index f195b4d21..55e2dab0d 100644 --- a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/guide-to-nginx-ssl-spdy-hsts/nginx.conf +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/guide-to-nginx-ssl-spdy-hsts/nginx.conf @@ -8,7 +8,7 @@ http { keepalive_timeout 60; # http-redirects to https; even if using of hsts; - # usefull if users are typing your server-name w/out https:// + # useful if users are typing your server-name w/out https:// # logjam and a good idea anyway @@ -98,7 +98,7 @@ http { #ssl_ciphers ALL:!ADH:!EXP:!LOW:!RC2:!3DES:!SEED:!RC4:+HIGH:+MEDIUM; # - # suggestions by mozilla-server-team - good compatibility, pfs, preferrable ciphers + # suggestions by mozilla-server-team - good compatibility, pfs, preferable ciphers # # modern ciphers ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK'; @@ -106,7 +106,7 @@ http { # intermediate ciphers #ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; - # old ciphers (would need SSLv3, but is not recommende as of oct 2014 + # old ciphers (would need SSLv3, but is not recommended as of oct 2014 #ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; # logjam / cipher suggested from weakdh.org diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/phplist/nginx.conf b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/phplist/nginx.conf index 9cf532809..357100558 100644 --- a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/phplist/nginx.conf +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/phplist/nginx.conf @@ -29,7 +29,7 @@ server { location ~* (index\.php|upload\.php|connector\.php|dl\.php|ut\.php|lt\.php|download\.php)$ { fastcgi_split_path_info ^(.|\.php)(/.+)$; - include /etc/nginx/fastcgi_params.conf; #standar fastcgi config file + include /etc/nginx/fastcgi_params.conf; #standard fastcgi config file fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_intercept_errors on; fastcgi_pass 127.0.0.1:9000; diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index e515ede7b..e2d226a72 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.11.0.dev0' +version = '0.12.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index f19a07910..6d51ca641 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -403,25 +403,7 @@ class NginxConfigurator(common.Plugin): except (socket.error, socket.herror, socket.timeout): continue - return self._get_filtered_names(all_names) - - def _get_filtered_names(self, all_names): - """Removes names that aren't considered valid by Let's Encrypt. - - :param set all_names: all names found in the Nginx configuration - - :returns: all found names that are considered valid by LE - :rtype: set - - """ - filtered_names = set() - for name in all_names: - try: - filtered_names.add(util.enforce_le_validity(name)) - except errors.ConfigurationError as error: - logger.debug('Not suggesting name "%s"', name) - logger.debug(error) - return filtered_names + return util.get_filtered_names(all_names) def _get_snakeoil_paths(self): # TODO: generate only once diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 2cbd0b86a..f6437c589 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -52,10 +52,10 @@ class RawNginxParser(object): map_statement = space + Literal("map") + space + nonspace + space + dollar_var + space # This is NOT an accurate way to parse nginx map entries; it's almost - # certianly too permissive and may be wrong in other ways, but it should + # certainly too permissive and may be wrong in other ways, but it should # preserve things correctly in mmmmost or all cases. # - # - I can neither prove nor disprove that it is corect wrt all escaped + # - I can neither prove nor disprove that it is correct wrt all escaped # semicolon situations # Addresses https://github.com/fatiherikli/nginxparser/issues/19 map_pattern = Regex(r'".*"') | Regex(r"'.*'") | nonspace @@ -143,7 +143,7 @@ class RawNginxDumper(object): def loads(source): """Parses from a string. - :param str souce: The string to parse + :param str source: The string to parse :returns: The parsed tree :rtype: list diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules index c9220209f..9826e02cb 100644 --- a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules +++ b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules @@ -67,7 +67,7 @@ MainRule "rx:%[2|3]." "msg:double encoding !" "mz:ARGS|URL|BODY|$HEADERS_VAR:Co #################################### MainRule "str:&#" "msg: utf7/8 encoding" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$EVADE:4" id:1400; MainRule "str:%U" "msg: M$ encoding" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$EVADE:4" id:1401; -MainRule negative "rx:multipart/form-data|application/x-www-form-urlencoded" "msg:Content is neither mulipart/x-www-form.." "mz:$HEADERS_VAR:Content-type" "s:$EVADE:4" id:1402; +MainRule negative "rx:multipart/form-data|application/x-www-form-urlencoded" "msg:Content is neither multipart/x-www-form.." "mz:$HEADERS_VAR:Content-type" "s:$EVADE:4" id:1402; ############################# ## File uploads: 1500-1600 ## diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf index 774fd9fc9..cd2885292 100644 --- a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf +++ b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf @@ -36,7 +36,7 @@ charset_map windows-1251 utf-8 { AA D084; # capital Ukrainian YE AB C2AB; # left-pointing double angle quotation mark AC C2AC; # not sign - AD C2AD; # soft hypen + AD C2AD; # soft hyphen AE C2AE; # (R) AF D087; # capital Ukrainian YI diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index e140c75d2..6de2dc6bd 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.11.0.dev0' +version = '0.12.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 376b0504f..6451eb0d5 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.11.0.dev0' +__version__ = '0.12.0.dev0' diff --git a/certbot/achallenges.py b/certbot/achallenges.py index 5ee6d2945..c2af45fdb 100644 --- a/certbot/achallenges.py +++ b/certbot/achallenges.py @@ -1,6 +1,6 @@ """Client annotated ACME challenges. -Please use names such as ``achall`` to distiguish from variables "of type" +Please use names such as ``achall`` to distinguish from variables "of type" :class:`acme.challenges.Challenge` (denoted by ``chall``) and :class:`.ChallengeBody` (denoted by ``challb``):: diff --git a/certbot/auth_handler.py b/certbot/auth_handler.py index aad971eb6..6e9ab25a7 100644 --- a/certbot/auth_handler.py +++ b/certbot/auth_handler.py @@ -34,8 +34,7 @@ class AuthHandler(object): :ivar list achalls: DV challenges in the form of :class:`certbot.achallenges.AnnotatedChallenge` :ivar list pref_challs: sorted user specified preferred challenges - in the form of subclasses of :class:`acme.challenges.Challenge` - with the most preferred challenge listed first + type strings with the most preferred challenge listed first """ def __init__(self, auth, acme, account, pref_challs): @@ -252,8 +251,10 @@ class AuthHandler(object): # Make sure to make a copy... plugin_pref = self.auth.get_chall_pref(domain) if self.pref_challs: - chall_prefs.extend(pref for pref in self.pref_challs - if pref in plugin_pref) + plugin_pref_types = set(chall.typ for chall in plugin_pref) + for typ in self.pref_challs: + if typ in plugin_pref_types: + chall_prefs.append(challenges.Challenge.TYPES[typ]) if chall_prefs: return chall_prefs raise errors.AuthorizationError( diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py index ae4c5a722..71a5fe6fa 100644 --- a/certbot/cert_manager.py +++ b/certbot/cert_manager.py @@ -95,27 +95,23 @@ def delete(config): # Public Helpers ################### -def lineage_for_certname(config, certname): +def lineage_for_certname(cli_config, certname): """Find a lineage object with name certname.""" - def update_cert_for_name_match(candidate_lineage, rv): - """Return cert if it has name certname, else return rv - """ - matching_lineage_name_cert = rv - if candidate_lineage.lineagename == certname: - matching_lineage_name_cert = candidate_lineage - return matching_lineage_name_cert - return _search_lineages(config, update_cert_for_name_match, None) + configs_dir = cli_config.renewal_configs_dir + # Verify the directory is there + util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) + renewal_file = storage.renewal_file_for_certname(cli_config, certname) + try: + return storage.RenewableCert(renewal_file, cli_config) + except (errors.CertStorageError, IOError): + logger.debug("Renewal conf file %s is broken.", renewal_file) + logger.debug("Traceback was:\n%s", traceback.format_exc()) + return None def domains_for_certname(config, certname): """Find the domains in the cert with name certname.""" - def update_domains_for_name_match(candidate_lineage, rv): - """Return domains if certname matches, else return rv - """ - matching_domains = rv - if candidate_lineage.lineagename == certname: - matching_domains = candidate_lineage.names() - return matching_domains - return _search_lineages(config, update_domains_for_name_match, None) + lineage = lineage_for_certname(config, certname) + return lineage.names() if lineage else None def find_duplicative_certs(config, domains): """Find existing certs that duplicate the request.""" diff --git a/certbot/cli.py b/certbot/cli.py index 7d146e33b..d7fda7574 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1,4 +1,5 @@ """Certbot command line argument & config processing.""" +# pylint: disable=too-many-lines from __future__ import print_function import argparse import copy @@ -149,7 +150,7 @@ def possible_deprecation_warning(config): if cli_command != LEAUTO: return if config.no_self_upgrade: - # users setting --no-self-upgrade might be hanging on a clent version like 0.3.0 + # users setting --no-self-upgrade might be hanging on a client version like 0.3.0 # or 0.5.0 which is the new script, but doesn't set CERTBOT_AUTO; they don't # need warnings return @@ -317,7 +318,7 @@ class CustomHelpFormatter(argparse.HelpFormatter): # The attributes here are: # short: a string that will be displayed by "certbot -h commands" # opts: a string that heads the section of flags with which this command is documented, -# both for "cerbot -h SUBCOMMAND" and "certbot -h all" +# both for "certbot -h SUBCOMMAND" and "certbot -h all" # usage: an optional string that overrides the header of "certbot -h SUBCOMMAND" VERB_HELP = [ ("run (default)", { @@ -333,7 +334,7 @@ VERB_HELP = [ "This command obtains a TLS/SSL certificate without installing it anywhere.") }), ("renew", { - "short": "Renew all certificates (or one specifed with --cert-name)", + "short": "Renew all certificates (or one specified with --cert-name)", "opts": ("The 'renew' subcommand will attempt to renew all" " certificates (or more precisely, certificate lineages) you have" " previously obtained if they are close to expiry, and print a" @@ -438,7 +439,7 @@ class HelpfulArgumentParser(object): self.detect_defaults = detect_defaults self.args = args - if self.args[0] == 'help': + if self.args and self.args[0] == 'help': self.args[0] = '--help' self.determine_verb() @@ -495,7 +496,7 @@ class HelpfulArgumentParser(object): if "apache" in plugins: apache_doc = "--apache Use the Apache plugin for authentication & installation" else: - apache_doc = "(the cerbot apache plugin is not installed)" + apache_doc = "(the certbot apache plugin is not installed)" usage = SHORT_USAGE if help_arg == True: @@ -879,6 +880,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis helpful.add( ["register", "unregister", "automation"], "-m", "--email", help=config_help("email")) + helpful.add(["register", "automation"], "--eff-email", action="store_true", + default=None, dest="eff_email", + help="Share your e-mail address with EFF") + helpful.add(["register", "automation"], "--no-eff-email", action="store_false", + default=None, dest="eff_email", + help="Don't share your e-mail address with EFF") helpful.add( ["automation", "certonly", "run"], "--keep-until-expiring", "--keep", "--reinstall", @@ -1236,13 +1243,31 @@ class _PrefChallAction(argparse.Action): """Action class for parsing preferred challenges.""" def __call__(self, parser, namespace, pref_challs, option_string=None): - aliases = {"dns": "dns-01", "http": "http-01", "tls-sni": "tls-sni-01"} - challs = [c.strip() for c in pref_challs.split(",")] - challs = [aliases[c] if c in aliases else c for c in challs] - unrecognized = ", ".join(name for name in challs - if name not in challenges.Challenge.TYPES) - if unrecognized: - raise argparse.ArgumentTypeError( - "Unrecognized challenges: {0}".format(unrecognized)) - namespace.pref_challs.extend(challenges.Challenge.TYPES[name] - for name in challs) + try: + challs = parse_preferred_challenges(pref_challs.split(",")) + except errors.Error as error: + raise argparse.ArgumentTypeError(str(error)) + namespace.pref_challs.extend(challs) + + +def parse_preferred_challenges(pref_challs): + """Translate and validate preferred challenges. + + :param pref_challs: list of preferred challenge types + :type pref_challs: `list` of `str` + + :returns: validated list of preferred challenge types + :rtype: `list` of `str` + + :raises errors.Error: if pref_challs is invalid + + """ + aliases = {"dns": "dns-01", "http": "http-01", "tls-sni": "tls-sni-01"} + challs = [c.strip() for c in pref_challs] + challs = [aliases.get(c, c) for c in challs] + unrecognized = ", ".join(name for name in challs + if name not in challenges.Challenge.TYPES) + if unrecognized: + raise errors.Error( + "Unrecognized challenges: {0}".format(unrecognized)) + return challs diff --git a/certbot/client.py b/certbot/client.py index f76688b70..26c5e87a9 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -15,15 +15,16 @@ import certbot from certbot import account from certbot import auth_handler +from certbot import cli from certbot import constants from certbot import crypto_util -from certbot import errors +from certbot import eff from certbot import error_handler +from certbot import errors from certbot import interfaces -from certbot import util from certbot import reverter from certbot import storage -from certbot import cli +from certbot import util from certbot.display import ops as display_ops from certbot.display import enhancements @@ -93,7 +94,7 @@ def register(config, account_storage, tos_cb=None): Terms of Service present in the contained `.Registration.terms_of_service` is accepted by the client, and ``False`` otherwise. ``tos_cb`` will be called only if the - client acction is necessary, i.e. when ``terms_of_service is not + client action is necessary, i.e. when ``terms_of_service is not None``. This argument is optional, if not supplied it will default to automatic acceptance! @@ -139,6 +140,8 @@ def register(config, account_storage, tos_cb=None): account.report_new_account(acc, config) account_storage.save(acc) + eff.handle_subscription(config) + return acc, acme @@ -438,7 +441,7 @@ class Client(object): self.installer.restart() def apply_enhancement(self, domains, enhancement, options=None): - """Applies an enhacement on all domains. + """Applies an enhancement on all domains. :param domains: list of ssl_vhosts :type list of str @@ -494,7 +497,7 @@ class Client(object): self.installer.rollback_checkpoints() self.installer.restart() except: - # TODO: suggest letshelp-letsencypt here + # TODO: suggest letshelp-letsencrypt here reporter.add_message( "An error occurred and we failed to restore your config and " "restart your server. Please submit a bug report to " diff --git a/certbot/constants.py b/certbot/constants.py index f64ff7e1e..b286ca26a 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -106,3 +106,6 @@ RENEWAL_CONFIGS_DIR = "renewal" FORCE_INTERACTIVE_FLAG = "--force-interactive" """Flag to disable TTY checking in IDisplay.""" + +EFF_SUBSCRIBE_URI = "https://supporters.eff.org/subscribe/certbot" +"""EFF URI used to submit the e-mail address of users who opt-in.""" diff --git a/certbot/eff.py b/certbot/eff.py new file mode 100644 index 000000000..746261faa --- /dev/null +++ b/certbot/eff.py @@ -0,0 +1,95 @@ +"""Subscribes users to the EFF newsletter.""" +import logging + +import requests +import zope.component + +from certbot import constants +from certbot import interfaces + + +logger = logging.getLogger(__name__) + + +def handle_subscription(config): + """High level function to take care of EFF newsletter subscriptions. + + The user may be asked if they want to sign up for the newsletter if + they have not already specified. + + :param .IConfig config: Client configuration. + + """ + if config.email is None: + if config.eff_email: + _report_failure("you didn't provide an e-mail address") + return + if config.eff_email is None: + config.eff_email = _want_subscription() + if config.eff_email: + subscribe(config.email) + + +def _want_subscription(): + """Does the user want to be subscribed to the EFF newsletter? + + :returns: True if we should subscribe the user, otherwise, False + :rtype: bool + + """ + prompt = ( + 'Would you be willing to share your email address with the ' + "Electronic Frontier Foundation, a founding partner of the Let's " + 'Encrypt project and the non-profit organization that develops ' + "Certbot? We'd like to send you email about EFF and our work to " + 'encrypt the web, protect its users and defend digital rights.') + display = zope.component.getUtility(interfaces.IDisplay) + return display.yesno(prompt, default=False) + + +def subscribe(email): + """Subscribe the user to the EFF mailing list. + + :param str email: the e-mail address to subscribe + + """ + url = constants.EFF_SUBSCRIBE_URI + data = {'data_type': 'json', + 'email': email, + 'form_id': 'eff_supporters_library_subscribe_form'} + logger.debug('Sending POST request to %s:\n%s', url, data) + _check_response(requests.post(url, data=data)) + + +def _check_response(response): + """Check for errors in the server's response. + + If an error occurred, it will be reported to the user. + + :param requests.Response response: the server's response to the + subscription request + + """ + logger.debug('Received response:\n%s', response.content) + if response.ok: + if not response.json()['status']: + _report_failure('your e-mail address appears to be invalid') + else: + _report_failure() + + +def _report_failure(reason=None): + """Notify the user of failing to sign them up for the newsletter. + + :param reason: a phrase describing what the problem was + beginning with a lowercase letter and no closing punctuation + :type reason: `str` or `None` + + """ + msg = ['We were unable to subscribe you the EFF mailing list'] + if reason is not None: + msg.append(' because ') + msg.append(reason) + msg.append('. You can try again later by visiting https://act.eff.org.') + reporter = zope.component.getUtility(interfaces.IReporter) + reporter.add_message(''.join(msg), reporter.LOW_PRIORITY) diff --git a/certbot/main.py b/certbot/main.py index f8cb411b5..7ca346050 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -24,6 +24,7 @@ from certbot import crypto_util from certbot import colored_logging from certbot import configuration from certbot import constants +from certbot import eff from certbot import errors from certbot import hooks from certbot import interfaces @@ -474,6 +475,7 @@ def register(config, unused_plugins): acc.regr = acme_client.acme.update_registration(acc.regr.update( body=acc.regr.body.update(contact=('mailto:' + config.email,)))) account_storage.save_regr(acc) + eff.handle_subscription(config) add_msg("Your e-mail address was updated to {0}.".format(config.email)) @@ -812,7 +814,7 @@ def make_or_verify_core_dir(directory, mode, uid, strict): raise errors.Error(_PERM_ERR_FMT.format(error)) def make_or_verify_needed_dirs(config): - """Create or verify existance of config, work, or logs directories""" + """Create or verify existence of config, work, or logs directories""" make_or_verify_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), config.strict_permissions) make_or_verify_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE, diff --git a/certbot/plugins/disco.py b/certbot/plugins/disco.py index ba532eb1b..e567422e2 100644 --- a/certbot/plugins/disco.py +++ b/certbot/plugins/disco.py @@ -79,7 +79,7 @@ class PluginEntryPoint(object): return self._initialized is not None def init(self, config=None): - """Memoized plugin inititialization.""" + """Memoized plugin initialization.""" if not self.initialized: self.entry_point.require() # fetch extras! self._initialized = self.plugin_cls(config, self.name) @@ -230,7 +230,7 @@ class PluginsRegistry(collections.Mapping): def available(self): """Filter plugins based on availability.""" return self.filter(lambda p_ep: p_ep.available) - # succefully prepared + misconfigured + # successfully prepared + misconfigured def find_init(self, plugin): """Find an initialized plugin. diff --git a/certbot/plugins/disco_test.py b/certbot/plugins/disco_test.py index 7282c9ec8..6c3c39dca 100644 --- a/certbot/plugins/disco_test.py +++ b/certbot/plugins/disco_test.py @@ -255,7 +255,7 @@ class PluginsRegistryTest(unittest.TestCase): def test_find_init(self): self.assertTrue(self.reg.find_init(mock.Mock()) is None) - self.plugin_ep.initalized = True + self.plugin_ep.initialized = True self.assertTrue( self.reg.find_init(self.plugin_ep.init()) is self.plugin_ep) diff --git a/certbot/plugins/selection_test.py b/certbot/plugins/selection_test.py index eb4db2081..41c2b55c9 100644 --- a/certbot/plugins/selection_test.py +++ b/certbot/plugins/selection_test.py @@ -1,4 +1,4 @@ -"""Tests for letsenecrypt.plugins.selection""" +"""Tests for letsencrypt.plugins.selection""" import sys import unittest diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index 4fc52479f..0c15930a3 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -18,7 +18,6 @@ from certbot import errors from certbot import interfaces from certbot.plugins import common -from certbot.plugins import util logger = logging.getLogger(__name__) @@ -208,74 +207,38 @@ class Authenticator(common.Plugin): # pylint: disable=unused-argument,missing-docstring return self.supported_challenges - def _verify_ports_are_available(self, achalls): - """Confirm the ports are available to solve all achalls. - - :param list achalls: list of - :class:`~certbot.achallenges.AnnotatedChallenge` - - :raises .errors.MisconfigurationError: if required port is - unavailable - - """ - ports = [] - if any(isinstance(ac.chall, challenges.HTTP01) for ac in achalls): - ports.append(self.config.http01_port) - if any(isinstance(ac.chall, challenges.TLSSNI01) for ac in achalls): - ports.append(self.config.tls_sni_01_port) - - renewer = (self.config.verb == "renew") - - if any(util.already_listening(port, renewer) for port in ports): - raise errors.MisconfigurationError( - "At least one of the required ports is already taken.") - def perform(self, achalls): # pylint: disable=missing-docstring - self._verify_ports_are_available(achalls) + return [self._try_perform_single(achall) for achall in achalls] - try: - return self.perform2(achalls) - except errors.StandaloneBindError as error: - display = zope.component.getUtility(interfaces.IDisplay) + def _try_perform_single(self, achall): + while True: + try: + return self._perform_single(achall) + except errors.StandaloneBindError as error: + _handle_perform_error(error) - if error.socket_error.errno == socket.errno.EACCES: - display.notification( - "Could not bind TCP port {0} because you don't have " - "the appropriate permissions (for example, you " - "aren't running this program as " - "root).".format(error.port), force_interactive=True) - elif error.socket_error.errno == socket.errno.EADDRINUSE: - display.notification( - "Could not bind TCP port {0} because it is already in " - "use by another process on this system (such as a web " - "server). Please stop the program in question and then " - "try again.".format(error.port), force_interactive=True) - else: - raise # XXX: How to handle unknown errors in binding? + def _perform_single(self, achall): + if isinstance(achall.chall, challenges.HTTP01): + server, response = self._perform_http_01(achall) + else: # tls-sni-01 + server, response = self._perform_tls_sni_01(achall) + self.served[server].add(achall) + return response - def perform2(self, achalls): - """Perform achallenges without IDisplay interaction.""" - responses = [] + def _perform_http_01(self, achall): + server = self.servers.run(self.config.http01_port, challenges.HTTP01) + response, validation = achall.response_and_validation() + resource = acme_standalone.HTTP01RequestHandler.HTTP01Resource( + chall=achall.chall, response=response, validation=validation) + self.http_01_resources.add(resource) + return server, response - for achall in achalls: - if isinstance(achall.chall, challenges.HTTP01): - server = self.servers.run( - self.config.http01_port, challenges.HTTP01) - response, validation = achall.response_and_validation() - self.http_01_resources.add( - acme_standalone.HTTP01RequestHandler.HTTP01Resource( - chall=achall.chall, response=response, - validation=validation)) - else: # tls-sni-01 - server = self.servers.run( - self.config.tls_sni_01_port, challenges.TLSSNI01) - response, (cert, _) = achall.response_and_validation( - cert_key=self.key) - self.certs[response.z_domain] = (self.key, cert) - self.served[server].add(achall) - responses.append(response) - - return responses + def _perform_tls_sni_01(self, achall): + port = self.config.tls_sni_01_port + server = self.servers.run(port, challenges.TLSSNI01) + response, (cert, _) = achall.response_and_validation(cert_key=self.key) + self.certs[response.z_domain] = (self.key, cert) + return server, response def cleanup(self, achalls): # pylint: disable=missing-docstring # reduce self.served and close servers if none challenges are served @@ -286,3 +249,25 @@ class Authenticator(common.Plugin): for port, server in six.iteritems(self.servers.running()): if not self.served[server]: self.servers.stop(port) + + +def _handle_perform_error(error): + if error.socket_error.errno == socket.errno.EACCES: + raise errors.PluginError( + "Could not bind TCP port {0} because you don't have " + "the appropriate permissions (for example, you " + "aren't running this program as " + "root).".format(error.port)) + elif error.socket_error.errno == socket.errno.EADDRINUSE: + display = zope.component.getUtility(interfaces.IDisplay) + msg = ( + "Could not bind TCP port {0} because it is already in " + "use by another process on this system (such as a web " + "server). Please stop the program in question and " + "then try again.".format(error.port)) + should_retry = display.yesno(msg, "Retry", + "Cancel", default=False) + if not should_retry: + raise errors.PluginError(msg) + else: + raise diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index 08e59c929..83e0fcf7f 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -8,11 +8,9 @@ import six from acme import challenges from acme import jose -from acme import standalone as acme_standalone from certbot import achallenges from certbot import errors -from certbot import interfaces from certbot.tests import acme_util from certbot.tests import util as test_util @@ -114,6 +112,7 @@ def get_open_port(): open_socket.close() return port + class AuthenticatorTest(unittest.TestCase): """Tests for certbot.plugins.standalone.Authenticator.""" @@ -124,6 +123,7 @@ class AuthenticatorTest(unittest.TestCase): tls_sni_01_port=get_open_port(), http01_port=get_open_port(), standalone_supported_challenges="tls-sni-01,http-01") self.auth = Authenticator(self.config, name="standalone") + self.auth.servers = mock.MagicMock() def test_supported_challenges(self): self.assertEqual(self.auth.supported_challenges, @@ -146,6 +146,52 @@ class AuthenticatorTest(unittest.TestCase): self.assertEqual(self.auth.get_chall_pref(domain=None), [challenges.TLSSNI01]) + def test_perform(self): + achalls = self._get_achalls() + response = self.auth.perform(achalls) + + expected = [achall.response(achall.account_key) for achall in achalls] + self.assertEqual(response, expected) + + @test_util.patch_get_utility() + def test_perform_eaddrinuse_retry(self, mock_get_utility): + errno = socket.errno.EADDRINUSE + error = errors.StandaloneBindError(mock.MagicMock(errno=errno), -1) + self.auth.servers.run.side_effect = [error] + 2 * [mock.MagicMock()] + mock_yesno = mock_get_utility.return_value.yesno + mock_yesno.return_value = True + + self.test_perform() + self._assert_correct_yesno_call(mock_yesno) + + @test_util.patch_get_utility() + def test_perform_eaddrinuse_no_retry(self, mock_get_utility): + mock_yesno = mock_get_utility.return_value.yesno + mock_yesno.return_value = False + + errno = socket.errno.EADDRINUSE + self.assertRaises(errors.PluginError, self._fail_perform, errno) + self._assert_correct_yesno_call(mock_yesno) + + def _assert_correct_yesno_call(self, mock_yesno): + yesno_args, yesno_kwargs = mock_yesno.call_args + self.assertTrue("in use" in yesno_args[0]) + self.assertFalse(yesno_kwargs.get("default", True)) + + def test_perform_eacces(self): + errno = socket.errno.EACCES + self.assertRaises(errors.PluginError, self._fail_perform, errno) + + def test_perform_unexpected_socket_error(self): + errno = socket.errno.ENOTCONN + self.assertRaises( + errors.StandaloneBindError, self._fail_perform, errno) + + def _fail_perform(self, errno): + error = errors.StandaloneBindError(mock.MagicMock(errno=errno), -1) + self.auth.servers.run.side_effect = error + self.auth.perform(self._get_achalls()) + @classmethod def _get_achalls(cls): domain = b'localhost' @@ -157,84 +203,7 @@ class AuthenticatorTest(unittest.TestCase): return [http_01, tls_sni_01] - @mock.patch("certbot.plugins.standalone.util") - def test_perform_already_listening(self, mock_util): - http_01, tls_sni_01 = self._get_achalls() - - for achall, port in ((http_01, self.config.http01_port,), - (tls_sni_01, self.config.tls_sni_01_port)): - mock_util.already_listening.return_value = True - self.assertRaises( - errors.MisconfigurationError, self.auth.perform, [achall]) - mock_util.already_listening.assert_called_once_with(port, False) - mock_util.already_listening.reset_mock() - - @test_util.patch_get_utility() - def test_perform(self, unused_mock_get_utility): - achalls = self._get_achalls() - - self.auth.perform2 = mock.Mock(return_value=mock.sentinel.responses) - self.assertEqual(mock.sentinel.responses, self.auth.perform(achalls)) - self.auth.perform2.assert_called_once_with(achalls) - - @test_util.patch_get_utility() - def _test_perform_bind_errors(self, errno, achalls, mock_get_utility): - port = get_open_port() - def _perform2(unused_achalls): - raise errors.StandaloneBindError(mock.Mock(errno=errno), port) - - self.auth.perform2 = mock.MagicMock(side_effect=_perform2) - self.auth.perform(achalls) - mock_get_utility.assert_called_once_with(interfaces.IDisplay) - notification = mock_get_utility.return_value.notification - self.assertEqual(1, notification.call_count) - self.assertTrue(str(port) in notification.call_args[0][0]) - - def test_perform_eacces(self): - # pylint: disable=no-value-for-parameter - self._test_perform_bind_errors(socket.errno.EACCES, []) - - def test_perform_eaddrinuse(self): - # pylint: disable=no-value-for-parameter - self._test_perform_bind_errors(socket.errno.EADDRINUSE, []) - - def test_perfom_unknown_bind_error(self): - self.assertRaises( - errors.StandaloneBindError, self._test_perform_bind_errors, - socket.errno.ENOTCONN, []) - - def test_perform2(self): - http_01, tls_sni_01 = self._get_achalls() - - self.auth.servers = mock.MagicMock() - - def _run(port, tls): # pylint: disable=unused-argument - return "server{0}".format(port) - - self.auth.servers.run.side_effect = _run - responses = self.auth.perform2([http_01, tls_sni_01]) - - self.assertTrue(isinstance(responses, list)) - self.assertEqual(2, len(responses)) - self.assertTrue(isinstance(responses[0], challenges.HTTP01Response)) - self.assertTrue(isinstance(responses[1], challenges.TLSSNI01Response)) - - self.assertEqual(self.auth.servers.run.mock_calls, [ - mock.call(self.config.http01_port, challenges.HTTP01), - mock.call(self.config.tls_sni_01_port, challenges.TLSSNI01), - ]) - self.assertEqual(self.auth.served, { - "server" + str(self.config.tls_sni_01_port): set([tls_sni_01]), - "server" + str(self.config.http01_port): set([http_01]), - }) - self.assertEqual(1, len(self.auth.http_01_resources)) - self.assertEqual(1, len(self.auth.certs)) - self.assertEqual(list(self.auth.http_01_resources), [ - acme_standalone.HTTP01RequestHandler.HTTP01Resource( - acme_util.HTTP01, responses[0], mock.ANY)]) - def test_cleanup(self): - self.auth.servers = mock.Mock() self.auth.servers.running.return_value = { 1: "server1", 2: "server2", diff --git a/certbot/plugins/util.py b/certbot/plugins/util.py index 20b0fdce7..e45c26735 100644 --- a/certbot/plugins/util.py +++ b/certbot/plugins/util.py @@ -1,34 +1,11 @@ """Plugin utilities.""" import logging import os -import socket -import zope.component - -from acme import errors as acme_errors -from acme import util as acme_util - -from certbot import interfaces from certbot import util -PSUTIL_REQUIREMENT = "psutil>=2.2.1" - -try: - acme_util.activate(PSUTIL_REQUIREMENT) - import psutil # pragma: no cover - USE_PSUTIL = True -except acme_errors.DependencyError: # pragma: no cover - USE_PSUTIL = False - logger = logging.getLogger(__name__) -RENEWER_EXTRA_MSG = ( - " For automated renewal, you may want to use a script that stops" - " and starts your webserver. You can find an example at" - " https://certbot.eff.org/docs/using.html#renewal ." - " Alternatively you can use the webroot plugin to renew without" - " needing to stop and start your webserver.") - def path_surgery(cmd): """Attempt to perform PATH surgery to find cmd @@ -59,105 +36,3 @@ def path_surgery(cmd): logger.warning("Failed to find %s in%s PATH: %s", cmd, expanded, path) return False - - -def already_listening(port, renewer=False): - """Check if a process is already listening on the port. - - If so, also tell the user via a display notification. - - .. warning:: - On some operating systems, this function can only usefully be - run as root. - - :param int port: The TCP port in question. - :returns: True or False. - - """ - - if USE_PSUTIL: - return already_listening_psutil(port, renewer=renewer) - else: - logger.debug("Psutil not found, using simple socket check.") - return already_listening_socket(port, renewer=renewer) - - -def already_listening_socket(port, renewer=False): - """Simple socket based check to find out if port is already in use - - :param int port: The TCP port in question. - :returns: True or False - """ - - try: - testsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) - testsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - try: - testsocket.bind(("", port)) - except socket.error: - display = zope.component.getUtility(interfaces.IDisplay) - extra = "" - if renewer: - extra = RENEWER_EXTRA_MSG - display.notification( - "Port {0} is already in use by another process. This will " - "prevent us from binding to that port. Please stop the " - "process that is populating the port in question and try " - "again. {1}".format(port, extra), force_interactive=True) - return True - finally: - testsocket.close() - except socket.error: - pass - return False - - -def already_listening_psutil(port, renewer=False): - """Psutil variant of the open port check - - :param int port: The TCP port in question. - :returns: True or False. - - """ - try: - net_connections = psutil.net_connections() - except psutil.AccessDenied as error: - logger.info("Access denied when trying to list network " - "connections: %s. Are you root?", error) - # this function is just a pre-check that often causes false - # positives and problems in testing (c.f. #680 on Mac, #255 - # generally); we will fail later in bind() anyway - return False - - listeners = [conn.pid for conn in net_connections - if conn.status == 'LISTEN' and - conn.type == socket.SOCK_STREAM and - conn.laddr[1] == port] - try: - if listeners and listeners[0] is not None: - # conn.pid may be None if the current process doesn't have - # permission to identify the listening process! Additionally, - # listeners may have more than one element if separate - # sockets have bound the same port on separate interfaces. - # We currently only have UI to notify the user about one - # of them at a time. - pid = listeners[0] - name = psutil.Process(pid).name() - display = zope.component.getUtility(interfaces.IDisplay) - extra = "" - if renewer: - extra = RENEWER_EXTRA_MSG - display.notification( - "The program {0} (process ID {1}) is already listening " - "on TCP port {2}. This will prevent us from binding to " - "that port. Please stop the {0} program temporarily " - "and then try again.{3}".format(name, pid, port, extra), - force_interactive=True) - return True - except (psutil.NoSuchProcess, psutil.AccessDenied): - # Perhaps the result of a race where the process could have - # exited or relinquished the port (NoSuchProcess), or the result - # of an OS policy where we're not allowed to look up the process - # name (AccessDenied). - pass - return False diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py index b5d188835..947f24697 100644 --- a/certbot/plugins/util_test.py +++ b/certbot/plugins/util_test.py @@ -1,13 +1,9 @@ """Tests for certbot.plugins.util.""" import os -import socket import unittest import mock -from certbot.plugins.util import PSUTIL_REQUIREMENT -from certbot.tests import util as test_util - class PathSurgeryTest(unittest.TestCase): """Tests for certbot.plugins.path_surgery.""" @@ -34,140 +30,5 @@ class PathSurgeryTest(unittest.TestCase): self.assertTrue("/tmp" in os.environ["PATH"]) -class AlreadyListeningTest(unittest.TestCase): - """Tests for certbot.plugins.already_listening.""" - @classmethod - def _call(cls, *args, **kwargs): - from certbot.plugins.util import already_listening - return already_listening(*args, **kwargs) - - -class AlreadyListeningTestNoPsutil(AlreadyListeningTest): - """Tests for certbot.plugins.already_listening when - psutil is not available""" - @classmethod - def _call(cls, *args, **kwargs): - with mock.patch("certbot.plugins.util.USE_PSUTIL", False): - return super( - AlreadyListeningTestNoPsutil, cls)._call(*args, **kwargs) - - @test_util.patch_get_utility() - def test_ports_available(self, mock_getutil): - # Ensure we don't get error - with mock.patch("socket.socket.bind"): - self.assertFalse(self._call(80)) - self.assertFalse(self._call(80, True)) - self.assertEqual(mock_getutil.call_count, 0) - - @test_util.patch_get_utility() - def test_ports_blocked(self, mock_getutil): - with mock.patch("certbot.plugins.util.socket.socket.bind") as mock_bind: - mock_bind.side_effect = socket.error - self.assertTrue(self._call(80)) - self.assertTrue(self._call(80, True)) - with mock.patch("certbot.plugins.util.socket.socket") as mock_socket: - mock_socket.side_effect = socket.error - self.assertFalse(self._call(80)) - self.assertEqual(mock_getutil.call_count, 2) - - -@test_util.skip_unless(test_util.requirement_available(PSUTIL_REQUIREMENT), - "optional dependency psutil is not available") -class AlreadyListeningTestPsutil(AlreadyListeningTest): - """Tests for certbot.plugins.already_listening.""" - @mock.patch("certbot.plugins.util.psutil.net_connections") - @mock.patch("certbot.plugins.util.psutil.Process") - @test_util.patch_get_utility() - def test_race_condition(self, mock_get_utility, mock_process, mock_net): - # This tests a race condition, or permission problem, or OS - # incompatibility in which, for some reason, no process name can be - # found to match the identified listening PID. - import psutil - from psutil._common import sconn - conns = [ - sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30), - raddr=(), status="LISTEN", pid=None), - sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783), - raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234), - sconn(fd=-1, family=10, type=1, laddr=("::1", 54321), - raddr=("::1", 111), status="CLOSE_WAIT", pid=None), - sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17), - raddr=(), status="LISTEN", pid=4416)] - mock_net.return_value = conns - mock_process.side_effect = psutil.NoSuchProcess("No such PID") - # We simulate being unable to find the process name of PID 4416, - # which results in returning False. - self.assertFalse(self._call(17)) - self.assertEqual(mock_get_utility.generic_notification.call_count, 0) - mock_process.assert_called_once_with(4416) - - @mock.patch("certbot.plugins.util.psutil.net_connections") - @mock.patch("certbot.plugins.util.psutil.Process") - @test_util.patch_get_utility() - def test_not_listening(self, mock_get_utility, mock_process, mock_net): - from psutil._common import sconn - conns = [ - sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30), - raddr=(), status="LISTEN", pid=None), - sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783), - raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234), - sconn(fd=-1, family=10, type=1, laddr=("::1", 54321), - raddr=("::1", 111), status="CLOSE_WAIT", pid=None)] - mock_net.return_value = conns - mock_process.name.return_value = "inetd" - self.assertFalse(self._call(17)) - self.assertEqual(mock_get_utility.generic_notification.call_count, 0) - self.assertEqual(mock_process.call_count, 0) - - @mock.patch("certbot.plugins.util.psutil.net_connections") - @mock.patch("certbot.plugins.util.psutil.Process") - @test_util.patch_get_utility() - def test_listening_ipv4(self, mock_get_utility, mock_process, mock_net): - from psutil._common import sconn - conns = [ - sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30), - raddr=(), status="LISTEN", pid=None), - sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783), - raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234), - sconn(fd=-1, family=10, type=1, laddr=("::1", 54321), - raddr=("::1", 111), status="CLOSE_WAIT", pid=None), - sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17), - raddr=(), status="LISTEN", pid=4416)] - mock_net.return_value = conns - mock_process.name.return_value = "inetd" - result = self._call(17, True) - self.assertTrue(result) - self.assertEqual(mock_get_utility.call_count, 1) - mock_process.assert_called_once_with(4416) - - @mock.patch("certbot.plugins.util.psutil.net_connections") - @mock.patch("certbot.plugins.util.psutil.Process") - @test_util.patch_get_utility() - def test_listening_ipv6(self, mock_get_utility, mock_process, mock_net): - from psutil._common import sconn - conns = [ - sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30), - raddr=(), status="LISTEN", pid=None), - sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783), - raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234), - sconn(fd=-1, family=10, type=1, laddr=("::1", 54321), - raddr=("::1", 111), status="CLOSE_WAIT", pid=None), - sconn(fd=3, family=10, type=1, laddr=("::", 12345), raddr=(), - status="LISTEN", pid=4420), - sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17), - raddr=(), status="LISTEN", pid=4416)] - mock_net.return_value = conns - mock_process.name.return_value = "inetd" - result = self._call(12345) - self.assertTrue(result) - self.assertEqual(mock_get_utility.call_count, 1) - mock_process.assert_called_once_with(4420) - - @mock.patch("certbot.plugins.util.psutil.net_connections") - def test_access_denied_exception(self, mock_net): - import psutil - mock_net.side_effect = psutil.AccessDenied("") - self.assertFalse(self._call(12345)) - if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot/renewal.py b/certbot/renewal.py index bd07cfd07..6b61b0841 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -35,7 +35,7 @@ INT_CONFIG_ITEMS = ["rsa_key_size", "tls_sni_01_port", "http01_port"] BOOL_CONFIG_ITEMS = ["must_staple", "allow_subset_of_names"] CONFIG_ITEMS = set(itertools.chain( - BOOL_CONFIG_ITEMS, INT_CONFIG_ITEMS, STR_CONFIG_ITEMS)) + BOOL_CONFIG_ITEMS, INT_CONFIG_ITEMS, STR_CONFIG_ITEMS, ('pref_challs',))) def _reconstitute(config, full_path): @@ -165,6 +165,7 @@ def restore_required_config_elements(config, renewalparams): """ required_items = itertools.chain( + (("pref_challs", _restore_pref_challs),), six.moves.zip(BOOL_CONFIG_ITEMS, itertools.repeat(_restore_bool)), six.moves.zip(INT_CONFIG_ITEMS, itertools.repeat(_restore_int)), six.moves.zip(STR_CONFIG_ITEMS, itertools.repeat(_restore_str))) @@ -174,6 +175,28 @@ def restore_required_config_elements(config, renewalparams): setattr(config.namespace, item_name, value) +def _restore_pref_challs(unused_name, value): + """Restores preferred challenges from a renewal config file. + + If value is a `str`, it should be a single challenge type. + + :param str unused_name: option name + :param value: option value + :type value: `list` of `str` or `str` + + :returns: converted option value to be stored in the runtime config + :rtype: `list` of `str` + + :raises errors.Error: if value can't be converted to an bool + + """ + # If pref_challs has only one element, configobj saves the value + # with a trailing comma so it's parsed as a list. If this comma is + # removed by the user, the value is parsed as a str. + value = [value] if isinstance(value, str) else value + return cli.parse_preferred_challenges(value) + + def _restore_bool(name, value): """Restores an boolean key-value pair from a renewal config file. diff --git a/certbot/storage.py b/certbot/storage.py index af0e9d701..4f1cd0a9d 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -716,7 +716,7 @@ class RenewableCert(object): :returns: ``True`` if there is a complete version of this lineage with a larger version number than the current - version, and ``False`` otherwis + version, and ``False`` otherwise :rtype: bool """ diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index 441550fc8..046eb5ef1 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -176,7 +176,8 @@ class GetAuthorizationsTest(unittest.TestCase): mock_poll.side_effect = self._validate_all self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01) - self.handler.pref_challs.extend((challenges.HTTP01, challenges.DNS01,)) + self.handler.pref_challs.extend((challenges.HTTP01.typ, + challenges.DNS01.typ,)) self.handler.get_authorizations(["0"]) @@ -187,7 +188,7 @@ class GetAuthorizationsTest(unittest.TestCase): def test_preferred_challenges_not_supported(self): self.mock_net.request_domain_challenges.side_effect = functools.partial( gen_dom_authzr, challs=acme_util.CHALLENGES) - self.handler.pref_challs.append(challenges.HTTP01) + self.handler.pref_challs.append(challenges.HTTP01.typ) self.assertRaises( errors.AuthorizationError, self.handler.get_authorizations, ["0"]) diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index d7d1a3aff..1fa68d195 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -268,11 +268,11 @@ class LineageForCertnameTest(BaseCertManagerTest): """Tests for certbot.cert_manager.lineage_for_certname""" @mock.patch('certbot.util.make_or_verify_dir') - @mock.patch('certbot.storage.renewal_conf_files') + @mock.patch('certbot.storage.renewal_file_for_certname') @mock.patch('certbot.storage.RenewableCert') - def test_found_match(self, mock_renewable_cert, mock_renewal_conf_files, + def test_found_match(self, mock_renewable_cert, mock_renewal_conf_file, mock_make_or_verify_dir): - mock_renewal_conf_files.return_value = ["somefile.conf"] + mock_renewal_conf_file.return_value = "somefile.conf" mock_match = mock.Mock(lineagename="example.com") mock_renewable_cert.return_value = mock_match from certbot import cert_manager @@ -281,13 +281,10 @@ class LineageForCertnameTest(BaseCertManagerTest): self.assertTrue(mock_make_or_verify_dir.called) @mock.patch('certbot.util.make_or_verify_dir') - @mock.patch('certbot.storage.renewal_conf_files') - @mock.patch('certbot.storage.RenewableCert') - def test_no_match(self, mock_renewable_cert, mock_renewal_conf_files, + @mock.patch('certbot.storage.renewal_file_for_certname') + def test_no_match(self, mock_renewal_conf_file, mock_make_or_verify_dir): - mock_renewal_conf_files.return_value = ["somefile.conf"] - mock_match = mock.Mock(lineagename="other.com") - mock_renewable_cert.return_value = mock_match + mock_renewal_conf_file.return_value = "other.com.conf" from certbot import cert_manager self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"), None) @@ -298,11 +295,11 @@ class DomainsForCertnameTest(BaseCertManagerTest): """Tests for certbot.cert_manager.domains_for_certname""" @mock.patch('certbot.util.make_or_verify_dir') - @mock.patch('certbot.storage.renewal_conf_files') + @mock.patch('certbot.storage.renewal_file_for_certname') @mock.patch('certbot.storage.RenewableCert') - def test_found_match(self, mock_renewable_cert, mock_renewal_conf_files, + def test_found_match(self, mock_renewable_cert, mock_renewal_conf_file, mock_make_or_verify_dir): - mock_renewal_conf_files.return_value = ["somefile.conf"] + mock_renewal_conf_file.return_value = "somefile.conf" mock_match = mock.Mock(lineagename="example.com") domains = ["example.com", "example.org"] mock_match.names.return_value = domains @@ -313,15 +310,10 @@ class DomainsForCertnameTest(BaseCertManagerTest): self.assertTrue(mock_make_or_verify_dir.called) @mock.patch('certbot.util.make_or_verify_dir') - @mock.patch('certbot.storage.renewal_conf_files') - @mock.patch('certbot.storage.RenewableCert') - def test_no_match(self, mock_renewable_cert, mock_renewal_conf_files, + @mock.patch('certbot.storage.renewal_file_for_certname') + def test_no_match(self, mock_renewal_conf_file, mock_make_or_verify_dir): - mock_renewal_conf_files.return_value = ["somefile.conf"] - mock_match = mock.Mock(lineagename="example.com") - domains = ["example.com", "example.org"] - mock_match.names.return_value = domains - mock_renewable_cert.return_value = mock_match + mock_renewal_conf_file.return_value = "somefile.conf" from certbot import cert_manager self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "other.com"), None) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 1fa2004d3..5f4a4e2c7 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -8,6 +8,8 @@ import mock import six from six.moves import reload_module # pylint: disable=import-error +from acme import challenges + from certbot import cli from certbot import constants from certbot import errors @@ -50,7 +52,7 @@ class ParseTest(unittest.TestCase): return cli.prepare_and_parse_args(PLUGINS, *args, **kwargs) def _help_output(self, args): - "Run a command, and return the ouput string for scrutiny" + "Run a command, and return the output string for scrutiny" output = six.StringIO() with mock.patch('certbot.main.sys.stdout', new=output): @@ -58,6 +60,11 @@ class ParseTest(unittest.TestCase): self.assertRaises(SystemExit, self.parse, args, output) return output.getvalue() + def test_no_args(self): + namespace = self.parse([]) + for d in ('config_dir', 'logs_dir', 'work_dir'): + self.assertEqual(getattr(namespace, d), cli.flag_default(d)) + def test_install_abspath(self): cert = 'cert' key = 'key' @@ -178,12 +185,12 @@ class ParseTest(unittest.TestCase): self.assertEqual(namespace.domains, ['example.com', 'another.net']) def test_preferred_challenges(self): - from acme.challenges import HTTP01, TLSSNI01, DNS01 - short_args = ['--preferred-challenges', 'http, tls-sni-01, dns'] namespace = self.parse(short_args) - self.assertEqual(namespace.pref_challs, [HTTP01, TLSSNI01, DNS01]) + expected = [challenges.HTTP01.typ, + challenges.TLSSNI01.typ, challenges.DNS01.typ] + self.assertEqual(namespace.pref_challs, expected) short_args = ['--preferred-challenges', 'jumping-over-the-moon'] self.assertRaises(argparse.ArgumentTypeError, self.parse, short_args) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index f4b86fc7c..cc3bb098d 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -44,20 +44,25 @@ class RegisterTest(unittest.TestCase): def test_no_tos(self): with mock.patch("certbot.client.acme_client.Client") as mock_client: mock_client.register().terms_of_service = "http://tos" - with mock.patch("certbot.account.report_new_account"): - self.tos_cb.return_value = False - self.assertRaises(errors.Error, self._call) + with mock.patch("certbot.eff.handle_subscription") as mock_handle: + with mock.patch("certbot.account.report_new_account"): + self.tos_cb.return_value = False + self.assertRaises(errors.Error, self._call) + self.assertFalse(mock_handle.called) - self.tos_cb.return_value = True - self._call() + self.tos_cb.return_value = True + self._call() + self.assertTrue(mock_handle.called) - self.tos_cb = None - self._call() + self.tos_cb = None + self._call() + self.assertEqual(mock_handle.call_count, 2) def test_it(self): with mock.patch("certbot.client.acme_client.Client"): with mock.patch("certbot.account.report_new_account"): - self._call() + with mock.patch("certbot.eff.handle_subscription"): + self._call() @mock.patch("certbot.account.report_new_account") @mock.patch("certbot.client.display_ops.get_email") @@ -67,9 +72,11 @@ class RegisterTest(unittest.TestCase): msg = "DNS problem: NXDOMAIN looking up MX for example.com" mx_err = messages.Error.with_code('invalidContact', detail=msg) with mock.patch("certbot.client.acme_client.Client") as mock_client: - mock_client().register.side_effect = [mx_err, mock.MagicMock()] - self._call() - self.assertEqual(mock_get_email.call_count, 1) + with mock.patch("certbot.eff.handle_subscription") as mock_handle: + mock_client().register.side_effect = [mx_err, mock.MagicMock()] + self._call() + self.assertEqual(mock_get_email.call_count, 1) + self.assertTrue(mock_handle.called) @mock.patch("certbot.account.report_new_account") def test_email_invalid_noninteractive(self, _rep): @@ -77,8 +84,9 @@ class RegisterTest(unittest.TestCase): msg = "DNS problem: NXDOMAIN looking up MX for example.com" mx_err = messages.Error.with_code('invalidContact', detail=msg) with mock.patch("certbot.client.acme_client.Client") as mock_client: - mock_client().register.side_effect = [mx_err, mock.MagicMock()] - self.assertRaises(errors.Error, self._call) + with mock.patch("certbot.eff.handle_subscription"): + mock_client().register.side_effect = [mx_err, mock.MagicMock()] + self.assertRaises(errors.Error, self._call) def test_needs_email(self): self.config.email = None @@ -86,21 +94,25 @@ class RegisterTest(unittest.TestCase): @mock.patch("certbot.client.logger") def test_without_email(self, mock_logger): - with mock.patch("certbot.client.acme_client.Client"): - with mock.patch("certbot.account.report_new_account"): - self.config.email = None - self.config.register_unsafely_without_email = True - self.config.dry_run = False - self._call() - mock_logger.warning.assert_called_once_with(mock.ANY) + with mock.patch("certbot.eff.handle_subscription") as mock_handle: + with mock.patch("certbot.client.acme_client.Client"): + with mock.patch("certbot.account.report_new_account"): + self.config.email = None + self.config.register_unsafely_without_email = True + self.config.dry_run = False + self._call() + mock_logger.warning.assert_called_once_with(mock.ANY) + self.assertTrue(mock_handle.called) def test_unsupported_error(self): from acme import messages msg = "Test" mx_err = messages.Error(detail=msg, typ="malformed", title="title") with mock.patch("certbot.client.acme_client.Client") as mock_client: - mock_client().register.side_effect = [mx_err, mock.MagicMock()] - self.assertRaises(messages.Error, self._call) + with mock.patch("certbot.eff.handle_subscription") as mock_handle: + mock_client().register.side_effect = [mx_err, mock.MagicMock()] + self.assertRaises(messages.Error, self._call) + self.assertFalse(mock_handle.called) class ClientTestCommon(unittest.TestCase): diff --git a/certbot/tests/eff_test.py b/certbot/tests/eff_test.py new file mode 100644 index 000000000..fd9a61181 --- /dev/null +++ b/certbot/tests/eff_test.py @@ -0,0 +1,135 @@ +"""Tests for certbot.eff.""" +import unittest + +import mock + +from certbot import constants +from certbot.tests import util + + +class HandleSubscriptionTest(unittest.TestCase): + """Tests for certbot.eff.handle_subscription.""" + def setUp(self): + self.email = 'certbot@example.org' + self.config = mock.Mock(email=self.email, eff_email=None) + + def _call(self): + from certbot.eff import handle_subscription + return handle_subscription(self.config) + + @util.patch_get_utility() + @mock.patch('certbot.eff.subscribe') + def test_failure(self, mock_subscribe, mock_get_utility): + self.config.email = None + self.config.eff_email = True + self._call() + self.assertFalse(mock_subscribe.called) + self.assertFalse(mock_get_utility().yesno.called) + actual = mock_get_utility().add_message.call_args[0][0] + expected_part = "because you didn't provide an e-mail address" + self.assertTrue(expected_part in actual) + + @mock.patch('certbot.eff.subscribe') + def test_no_subscribe_with_no_prompt(self, mock_subscribe): + self.config.eff_email = False + with util.patch_get_utility() as mock_get_utility: + self._call() + self.assertFalse(mock_subscribe.called) + self._assert_no_get_utility_calls(mock_get_utility) + + @util.patch_get_utility() + @mock.patch('certbot.eff.subscribe') + def test_subscribe_with_no_prompt(self, mock_subscribe, mock_get_utility): + self.config.eff_email = True + self._call() + self._assert_subscribed(mock_subscribe) + self._assert_no_get_utility_calls(mock_get_utility) + + def _assert_no_get_utility_calls(self, mock_get_utility): + self.assertFalse(mock_get_utility().yesno.called) + self.assertFalse(mock_get_utility().add_message.called) + + @util.patch_get_utility() + @mock.patch('certbot.eff.subscribe') + def test_subscribe_with_prompt(self, mock_subscribe, mock_get_utility): + mock_get_utility().yesno.return_value = True + self._call() + self._assert_subscribed(mock_subscribe) + self.assertFalse(mock_get_utility().add_message.called) + self._assert_correct_yesno_call(mock_get_utility) + + def _assert_subscribed(self, mock_subscribe): + self.assertTrue(mock_subscribe.called) + self.assertEqual(mock_subscribe.call_args[0][0], self.email) + + @util.patch_get_utility() + @mock.patch('certbot.eff.subscribe') + def test_no_subscribe_with_prompt(self, mock_subscribe, mock_get_utility): + mock_get_utility().yesno.return_value = False + self._call() + self.assertFalse(mock_subscribe.called) + self.assertFalse(mock_get_utility().add_message.called) + self._assert_correct_yesno_call(mock_get_utility) + + def _assert_correct_yesno_call(self, mock_get_utility): + self.assertTrue(mock_get_utility().yesno.called) + call_args, call_kwargs = mock_get_utility().yesno.call_args + actual = call_args[0] + expected_part = 'Electronic Frontier Foundation' + self.assertTrue(expected_part in actual) + self.assertFalse(call_kwargs.get('default', True)) + + +class SubscribeTest(unittest.TestCase): + """Tests for certbot.eff.subscribe.""" + def setUp(self): + self.email = 'certbot@example.org' + self.json = {'status': True} + self.response = mock.Mock(ok=True) + self.response.json.return_value = self.json + + @mock.patch('certbot.eff.requests.post') + def _call(self, mock_post): + mock_post.return_value = self.response + + from certbot.eff import subscribe + subscribe(self.email) + self._check_post_call(mock_post) + + def _check_post_call(self, mock_post): + self.assertEqual(mock_post.call_count, 1) + call_args, call_kwargs = mock_post.call_args + self.assertEqual(call_args[0], constants.EFF_SUBSCRIBE_URI) + + data = call_kwargs.get('data') + self.assertFalse(data is None) + self.assertEqual(data.get('email'), self.email) + + @util.patch_get_utility() + def test_bad_status(self, mock_get_utility): + self.json['status'] = False + self._call() # pylint: disable=no-value-for-parameter + actual = self._get_reported_message(mock_get_utility) + expected_part = 'because your e-mail address appears to be invalid.' + self.assertTrue(expected_part in actual) + + @util.patch_get_utility() + def test_not_ok(self, mock_get_utility): + self.response.ok = False + self._call() # pylint: disable=no-value-for-parameter + actual = self._get_reported_message(mock_get_utility) + unexpected_part = 'because' + self.assertFalse(unexpected_part in actual) + + def _get_reported_message(self, mock_get_utility): + self.assertTrue(mock_get_utility().add_message.called) + return mock_get_utility().add_message.call_args[0][0] + + @util.patch_get_utility() + def test_subscribe(self, mock_get_utility): + self._call() # pylint: disable=no-value-for-parameter + self.assertFalse(mock_get_utility.called) + + +if __name__ == '__main__': + unittest.main() # pragma: no cover diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index a548377bd..60dcf5e99 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -70,7 +70,7 @@ class ErrorHandlerTest(unittest.TestCase): send_signal(self.signals[0]) should_be_42 *= 10 - # check exectuion stoped when the signal was sent + # check execution stoped when the signal was sent self.assertEqual(42, should_be_42) # assert signals were caught self.assertEqual([self.signals[0]], signals_received) diff --git a/certbot/tests/errors_test.py b/certbot/tests/errors_test.py index f35a5ea08..aee1857a6 100644 --- a/certbot/tests/errors_test.py +++ b/certbot/tests/errors_test.py @@ -9,7 +9,7 @@ from certbot import achallenges from certbot.tests import acme_util -class FaiiledChallengesTest(unittest.TestCase): +class FailedChallengesTest(unittest.TestCase): """Tests for certbot.errors.FailedChallenges.""" def setUp(self): diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index a4f3a7805..22b709f69 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1145,9 +1145,9 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods def test_update_registration_with_email(self, mock_utility, mock_email): email = "user@example.com" mock_email.return_value = email - with mock.patch('certbot.main.client') as mocked_client: - with mock.patch('certbot.main.account') as mocked_account: - with mock.patch('certbot.main._determine_account') as mocked_det: + with mock.patch('certbot.eff.handle_subscription') as mock_handle: + with mock.patch('certbot.main._determine_account') as mocked_det: + with mock.patch('certbot.main.account') as mocked_account: with mock.patch('certbot.main.client') as mocked_client: mocked_storage = mock.MagicMock() mocked_account.AccountFileStorage.return_value = mocked_storage @@ -1168,6 +1168,7 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertTrue(mocked_storage.save_regr.called) self.assertTrue( email in mock_utility().add_message.call_args[0][0]) + self.assertTrue(mock_handle.called) class UnregisterTest(unittest.TestCase): diff --git a/certbot/tests/renewal_test.py b/certbot/tests/renewal_test.py index 2fe4eab68..cd53aa91c 100644 --- a/certbot/tests/renewal_test.py +++ b/certbot/tests/renewal_test.py @@ -5,6 +5,8 @@ import unittest import shutil import tempfile +from acme import challenges + from certbot import configuration from certbot import errors from certbot import storage @@ -59,6 +61,29 @@ class RestoreRequiredConfigElementsTest(unittest.TestCase): self.assertRaises( errors.Error, self._call, self.config, renewalparams) + @mock.patch('certbot.renewal.cli.set_by_cli') + def test_pref_challs_list(self, mock_set_by_cli): + mock_set_by_cli.return_value = False + renewalparams = {'pref_challs': 'tls-sni, http-01, dns'.split(',')} + self._call(self.config, renewalparams) + expected = [challenges.TLSSNI01.typ, + challenges.HTTP01.typ, challenges.DNS01.typ] + self.assertEqual(self.config.namespace.pref_challs, expected) + + @mock.patch('certbot.renewal.cli.set_by_cli') + def test_pref_challs_str(self, mock_set_by_cli): + mock_set_by_cli.return_value = False + renewalparams = {'pref_challs': 'dns'} + self._call(self.config, renewalparams) + expected = [challenges.DNS01.typ] + self.assertEqual(self.config.namespace.pref_challs, expected) + + @mock.patch('certbot.renewal.cli.set_by_cli') + def test_pref_challs_failure(self, mock_set_by_cli): + mock_set_by_cli.return_value = False + renewalparams = {'pref_challs': 'finding-a-shrubbery'} + self.assertRaises(errors.Error, self._call, self.config, renewalparams) + @mock.patch('certbot.renewal.cli.set_by_cli') def test_must_staple_success(self, mock_set_by_cli): mock_set_by_cli.return_value = False diff --git a/certbot/tests/reverter_test.py b/certbot/tests/reverter_test.py index 2eeabe116..d430f8292 100644 --- a/certbot/tests/reverter_test.py +++ b/certbot/tests/reverter_test.py @@ -394,7 +394,7 @@ class TestFullCheckpointsReverter(unittest.TestCase): self.assertTrue(mock_logger.info.call_count > 0) def test_view_config_changes_bad_backups_dir(self): - # There shouldn't be any "in progess directories when this is called + # There shouldn't be any "in progress directories when this is called # It must just be clean checkpoints os.makedirs(os.path.join(self.config.backup_dir, "in_progress")) diff --git a/certbot/tests/util.py b/certbot/tests/util.py index 7d674e171..092807b56 100644 --- a/certbot/tests/util.py +++ b/certbot/tests/util.py @@ -13,9 +13,7 @@ from cryptography.hazmat.primitives import serialization import mock import OpenSSL -from acme import errors from acme import jose -from acme import util from certbot import constants from certbot import interfaces @@ -86,20 +84,6 @@ def load_pyopenssl_private_key(*names): return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) -def requirement_available(requirement): - """Checks if requirement can be imported. - - :rtype: bool - :returns: ``True`` iff requirement can be imported - - """ - try: - util.activate(requirement) - except errors.DependencyError: # pragma: no cover - return False - return True # pragma: no cover - - def skip_unless(condition, reason): # pragma: no cover """Skip tests unless a condition holds. diff --git a/certbot/util.py b/certbot/util.py index e8532fc6d..95c669d0d 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -219,6 +219,25 @@ def safely_remove(path): raise +def get_filtered_names(all_names): + """Removes names that aren't considered valid by Let's Encrypt. + + :param set all_names: all names found in the configuration + + :returns: all found names that are considered valid by LE + :rtype: set + + """ + filtered_names = set() + for name in all_names: + try: + filtered_names.add(enforce_le_validity(name)) + except errors.ConfigurationError as error: + logger.debug('Not suggesting name "%s"', name) + logger.debug(error) + return filtered_names + + def get_os_info(filepath="/etc/os-release"): """ Get OS name and version diff --git a/docs/ciphers.rst b/docs/ciphers.rst index cf9ffdb99..31ce45963 100644 --- a/docs/ciphers.rst +++ b/docs/ciphers.rst @@ -206,7 +206,7 @@ or a family of enhancements, one per selectable ciphersuite configuration. Feedback ======== -We receive lots of feedback on the type of ciphersuites that Let's Encrypt supports and list some coallated feedback below. This section aims to track suggestions and references that people have offered or identified to improve the ciphersuites that Let's Encrypt enables when configuring TLS on servers. +We receive lots of feedback on the type of ciphersuites that Let's Encrypt supports and list some collated feedback below. This section aims to track suggestions and references that people have offered or identified to improve the ciphersuites that Let's Encrypt enables when configuring TLS on servers. Because of the Chatham House Rule applicable to some of the discussions, people are *not* individually credited for their suggestions, but most suggestions here were made or found by other people, and I thank them for their contributions. diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 672704ebc..5cc6c50a9 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -86,7 +86,7 @@ optional arguments: statistics about success rates by OS and plugin. If you wish to hide your server OS version from the Let's Encrypt server, set this to "". (default: - CertbotACMEClient/0.10.1 (Ubuntu 16.04.1 LTS) + CertbotACMEClient/0.11.0 (Ubuntu 16.04.1 LTS) Authenticator/XXX Installer/YYY) automation: @@ -120,7 +120,6 @@ automation: system. This option cannot be used with --csr. (default: False) --agree-tos Agree to the ACME Subscriber Agreement (default: Ask) - --account ACCOUNT_ID Account ID to use (default: None) --duplicate Allow making a certificate lineage that duplicates an existing one (both can be renewed in parallel) (default: False) @@ -210,7 +209,7 @@ manage: certificates List certificates managed by Certbot delete Clean up all files related to a certificate - renew Renew all certificates (or one specifed with --cert- + renew Renew all certificates (or one specified with --cert- name) revoke Revoke a certificate specified with --cert-path update_symlinks Recreate symlinks in your /etc/letsencrypt/live/ @@ -280,6 +279,9 @@ delete: revoke: Options for revocation of certs + --reason {keycompromise,affiliationchanged,superseded,unspecified,cessationofoperation} + Specify reason for revoking certificate. (default: 0) + register: Options for account registration & modification @@ -301,6 +303,14 @@ register: -m EMAIL, --email EMAIL Email used for registration and recovery contact. (default: Ask) + --eff-email Share your e-mail address with EFF (default: None) + --no-eff-email Don't share your e-mail address with EFF (default: + None) + +unregister: + Options for account deactivation. + + --account ACCOUNT_ID Account ID to use (default: None) install: Options for modifying how a cert is deployed diff --git a/docs/contributing.rst b/docs/contributing.rst index c51e493bc..040c22864 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -336,7 +336,7 @@ the ``letsencrypt-auto-source`` and Building letsencrypt-auto-source/letsencrypt-auto ------------------------------------------------- Once changes to any of the aforementioned files have been made, the -``letesncrypt-auto-source/letsencrypt-auto`` script should be updated. In lieu of +``letsencrypt-auto-source/letsencrypt-auto`` script should be updated. In lieu of manually updating this script, run the build script, which lives at ``letsencrypt-auto-source/build.py``: diff --git a/docs/using.rst b/docs/using.rst index a1881852e..0d74e4f8b 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -30,7 +30,7 @@ control the domain(s) you are requesting a cert for, obtains a cert for the spec domain(s), and places the cert in the ``/etc/letsencrypt`` directory on your machine. The authenticator does not install the cert (it does not edit any of your server's configuration files to serve the obtained certificate). If you specify multiple domains to authenticate, they will -all be listed in a single certificate. To obtain multiple seperate certificates +all be listed in a single certificate. To obtain multiple separate certificates you will need to run Certbot multiple times. Installers are Plugins used with the ``install`` command to install a cert. @@ -62,7 +62,7 @@ manual_ Y N | Helps you obtain a cert by giving you instructions to pe Under the hood, plugins use one of several ACME protocol "Challenges_" to prove you control a domain. The options are http-01_ (which uses port 80), -tls-sni-01_ (port 443) and dns-01_ (requring configuration of a DNS server on +tls-sni-01_ (port 443) and dns-01_ (requiring configuration of a DNS server on port 53, though that's often not the same machine as your webserver). A few plugins support more than one challenge type, in which case you can choose one with ``--preferred-challenges``. @@ -433,7 +433,7 @@ variables to these scripts: - ``CERTBOT_DOMAIN``: The domain being authenticated - ``CERTBOT_VALIDATION``: The validation string -- ``CERTBOT_TOKEN``: Resource name part of the HTTP-01 challenege (HTTP-01 only) +- ``CERTBOT_TOKEN``: Resource name part of the HTTP-01 challenge (HTTP-01 only) Additionally for cleanup: diff --git a/letsencrypt-auto b/letsencrypt-auto index b22916997..90b8157fe 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -23,7 +23,7 @@ if [ -z "$VENV_PATH" ]; then VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" fi VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.10.1" +LE_AUTO_VERSION="0.11.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -38,8 +38,9 @@ Help for certbot itself cannot be provided until it is installed. -n, --non-interactive, --noninteractive run without asking for user input --no-self-upgrade do not download updates --os-packages-only install OS dependencies and exit - -q, --quiet provide only update/error output -v, --verbose provide more output + -q, --quiet provide only update/error output; + implies --non-interactive All arguments are accepted and forwarded to the Certbot client when run." @@ -84,6 +85,11 @@ if [ $BASENAME = "letsencrypt-auto" ]; then HELP=0 fi +# Set ASSUME_YES to 1 if QUIET (i.e. --quiet implies --non-interactive) +if [ "$QUIET" = 1 ]; then + ASSUME_YES=1 +fi + # Support for busybox and others where there is no "command", # but "which" instead if command -v command > /dev/null 2>&1 ; then @@ -99,7 +105,7 @@ fi # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but # this script *can* be run as root (not recommended), or fall back to using -# `su`. Auto-detection can be overrided by explicitly setting the +# `su`. Auto-detection can be overridden by explicitly setting the # environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below. # Because the parameters in `su -c` has to be a string, @@ -207,7 +213,11 @@ BootstrapDebCommon() { # # - Debian 6.0.10 "squeeze" (x64) - $SUDO apt-get update || echo apt-get update hit problems but continuing anyway... + if [ "$QUIET" = 1 ]; then + QUIET_FLAG='-qq' + fi + + $SUDO apt-get $QUIET_FLAG update || echo apt-get update hit problems but continuing anyway... # virtualenv binary can be found in different packages depending on # distro version (#346) @@ -215,74 +225,74 @@ BootstrapDebCommon() { virtualenv= # virtual env is known to apt and is installable if apt-cache show virtualenv > /dev/null 2>&1 ; then - if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then - virtualenv="virtualenv" - fi + if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then + virtualenv="virtualenv" + fi fi if apt-cache show python-virtualenv > /dev/null 2>&1; then - virtualenv="$virtualenv python-virtualenv" + virtualenv="$virtualenv python-virtualenv" fi augeas_pkg="libaugeas0 augeas-lenses" AUGVERSION=`LC_ALL=C apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2` if [ "$ASSUME_YES" = 1 ]; then - YES_FLAG="-y" + YES_FLAG="-y" fi AddBackportRepo() { - # ARGS: - BACKPORT_NAME="$1" - BACKPORT_SOURCELINE="$2" - echo "To use the Apache Certbot plugin, augeas needs to be installed from $BACKPORT_NAME." - if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then - # This can theoretically error if sources.list.d is empty, but in that case we don't care. - if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then - if [ "$ASSUME_YES" = 1 ]; then - /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." - sleep 1s - /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." - sleep 1s - /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." - sleep 1s - add_backports=1 - else - read -p "Would you like to enable the $BACKPORT_NAME repository [Y/n]? " response - case $response in - [yY][eE][sS]|[yY]|"") - add_backports=1;; - *) - add_backports=0;; - esac - fi - if [ "$add_backports" = 1 ]; then - $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" - $SUDO apt-get update - fi - fi - fi - if [ "$add_backports" != 0 ]; then - $SUDO apt-get install $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg - augeas_pkg= + # ARGS: + BACKPORT_NAME="$1" + BACKPORT_SOURCELINE="$2" + echo "To use the Apache Certbot plugin, augeas needs to be installed from $BACKPORT_NAME." + if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then + # This can theoretically error if sources.list.d is empty, but in that case we don't care. + if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then + if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." + sleep 1s + add_backports=1 + else + read -p "Would you like to enable the $BACKPORT_NAME repository [Y/n]? " response + case $response in + [yY][eE][sS]|[yY]|"") + add_backports=1;; + *) + add_backports=0;; + esac + fi + if [ "$add_backports" = 1 ]; then + $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" + $SUDO apt-get $QUIET_FLAG update + fi fi + fi + if [ "$add_backports" != 0 ]; then + $SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg + augeas_pkg= + fi } if dpkg --compare-versions 1.0 gt "$AUGVERSION" ; then - if lsb_release -a | grep -q wheezy ; then - AddBackportRepo wheezy-backports "deb http://http.debian.net/debian wheezy-backports main" - elif lsb_release -a | grep -q precise ; then - # XXX add ARM case - AddBackportRepo precise-backports "deb http://archive.ubuntu.com/ubuntu precise-backports main restricted universe multiverse" - else - echo "No libaugeas0 version is available that's new enough to run the" - echo "Certbot apache plugin..." - fi - # XXX add a case for ubuntu PPAs + if lsb_release -a | grep -q wheezy ; then + AddBackportRepo wheezy-backports "deb http://http.debian.net/debian wheezy-backports main" + elif lsb_release -a | grep -q precise ; then + # XXX add ARM case + AddBackportRepo precise-backports "deb http://archive.ubuntu.com/ubuntu precise-backports main restricted universe multiverse" + else + echo "No libaugeas0 version is available that's new enough to run the" + echo "Certbot apache plugin..." + fi + # XXX add a case for ubuntu PPAs fi - $SUDO apt-get install $YES_FLAG --no-install-recommends \ + $SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \ python \ python-dev \ $virtualenv \ @@ -294,7 +304,6 @@ BootstrapDebCommon() { ca-certificates \ - if ! $EXISTS virtualenv > /dev/null ; then echo Failed to install a working \"virtualenv\" command, exiting exit 1 @@ -323,6 +332,9 @@ BootstrapRpmCommon() { if [ "$ASSUME_YES" = 1 ]; then yes_flag="-y" fi + if [ "$QUIET" = 1 ]; then + QUIET_FLAG='--quiet' + fi if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then echo "To use Certbot, packages from the EPEL repository need to be installed." @@ -331,14 +343,14 @@ BootstrapRpmCommon() { exit 1 fi if [ "$ASSUME_YES" = 1 ]; then - /bin/echo -n "Enabling the EPEL repository in 3 seconds..." - sleep 1s - /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." - sleep 1s - /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." - sleep 1s + /bin/echo -n "Enabling the EPEL repository in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." + sleep 1s fi - if ! $SUDO $tool install $yes_flag epel-release; then + if ! $SUDO $tool install $yes_flag $QUIET_FLAG epel-release; then echo "Could not enable EPEL. Aborting bootstrap!" exit 1 fi @@ -380,9 +392,9 @@ BootstrapRpmCommon() { " fi - if ! $SUDO $tool install $yes_flag $pkgs; then - echo "Could not install OS dependencies. Aborting bootstrap!" - exit 1 + if ! $SUDO $tool install $yes_flag $QUIET_FLAG $pkgs; then + echo "Could not install OS dependencies. Aborting bootstrap!" + exit 1 fi } @@ -394,7 +406,11 @@ BootstrapSuseCommon() { install_flags="-l" fi - $SUDO zypper $zypper_flags in $install_flags \ + if [ "$QUIET" = 1 ]; then + QUIET_FLAG='-qq' + fi + + $SUDO zypper $QUIET_FLAG $zypper_flags in $install_flags \ python \ python-devel \ python-virtualenv \ @@ -432,7 +448,11 @@ BootstrapArchCommon() { fi if [ "$missing" ]; then - $SUDO pacman -S --needed $missing $noconfirm + if [ "$QUIET" = 1]; then + $SUDO pacman -S --needed $missing $noconfirm > /dev/null + else + $SUDO pacman -S --needed $missing $noconfirm + fi fi } @@ -465,7 +485,11 @@ BootstrapGentooCommon() { } BootstrapFreeBsd() { - $SUDO pkg install -Ay \ + if [ "$QUIET" = 1 ]; then + QUIET_FLAG="--quiet" + fi + + $SUDO pkg install -Ay $QUIET_FLAG \ python \ py27-virtualenv \ augeas \ @@ -505,15 +529,15 @@ BootstrapMac() { fi if ! hash pip 2>/dev/null; then - echo "pip not installed" - echo "Installing pip..." - curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | python + echo "pip not installed" + echo "Installing pip..." + curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | python fi if ! hash virtualenv 2>/dev/null; then - echo "virtualenv not installed." - echo "Installing with pip..." - pip install virtualenv + echo "virtualenv not installed." + echo "Installing with pip..." + pip install virtualenv fi } @@ -523,26 +547,29 @@ BootstrapSmartOS() { } BootstrapMageiaCommon() { - if ! $SUDO urpmi --force \ - python \ - libpython-devel \ - python-virtualenv + if [ "$QUIET" = 1 ]; then + QUIET_FLAG='--quiet' + fi + + if ! $SUDO urpmi --force $QUIET_FLAG \ + python \ + libpython-devel \ + python-virtualenv then echo "Could not install Python dependencies. Aborting bootstrap!" exit 1 - fi + fi - if ! $SUDO urpmi --force \ - git \ - gcc \ - python-augeas \ - openssl \ - libopenssl-devel \ - libffi-devel \ - rootcerts + if ! $SUDO urpmi --force $QUIET_FLAG \ + git \ + gcc \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts then - echo "Could not install additional dependencies. Aborting bootstrap!" - exit 1 + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 fi } @@ -609,6 +636,11 @@ if [ "$1" = "--le-auto-phase2" ]; then # --version output ran through grep due to python-cryptography DeprecationWarnings # grep for both certbot and letsencrypt until certbot and shim packages have been released INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep "^certbot\|^letsencrypt" | cut -d " " -f 2) + if [ -z "$INSTALLED_VERSION" ]; then + echo "Error: couldn't get currently installed version for $VENV_BIN/letsencrypt: " 1>&2 + "$VENV_BIN/letsencrypt" --version + exit 1 + fi else INSTALLED_VERSION="none" fi @@ -801,18 +833,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.10.1 \ - --hash=sha256:1dd5124078bc44739065409f3be51765608a90994c83460578c2680c582c1026 \ - --hash=sha256:f51c2fb0a31646364abeb7fdd8cfc7c8a4e63b0641b14ab3ce1b2e3a8921a211 -certbot==0.10.1 \ - --hash=sha256:9a56fc76f726beeed2f5a08d690088377cd430907f8a38c50e2aa9a258ee1253 \ - --hash=sha256:e0d699adb3f8ca3e077a4db339de29ebb3f790fbc5f3f02e446e227ed40aa743 -certbot-apache==0.10.1 \ - --hash=sha256:1252fd7e435ba48484b0bd9b72535a9755b03d8f0440f164b9c1c560d96cadb8 \ - --hash=sha256:134f46690da55262125defa58aa74472eb4a1555c9ed83edb3c8667df5a561b5 -certbot-nginx==0.10.1 \ - --hash=sha256:afd15ed9e4f3076056b63916f272b7287084d871cb8136477d16b08f64d514f0 \ - --hash=sha256:da1b7ea4831ead3f9eb526ee11bf1bf197da0fea4defeeb7b1ce24c5d3f45b51 +acme==0.11.0 \ + --hash=sha256:9c084f9a62241a11231af63266f2f12ad696be590393a4ab4974276a47d63404 \ + --hash=sha256:1513ae74ee8424c739a953a552890830315669d95d105469d1cff5b7db9ae888 +certbot==0.11.0 \ + --hash=sha256:cc94d890a8697b3bdbdc555bcf4ba93955f64324bc256b2ea710fd053fc03b8a \ + --hash=sha256:0f91fee360f9ce5e0584d0954fa3123832435f77f465915389032a90ac0248b1 +certbot-apache==0.11.0 \ + --hash=sha256:9a01883ca7e1159cff2a6e36bf97b83793c899c62944335f6ac2f59f9b3e8b5d \ + --hash=sha256:2b67871e7ae8bbfa7a2779fcd6444f28847fbb7a347ef4bfdb19fb55a0d75673 +certbot-nginx==0.11.0 \ + --hash=sha256:cfae45a42560e39889eebd287437556084c421a9e07a2deb3cbc0aeef92d2dab \ + --hash=sha256:dbddffe47c8e8b2d1cf47fe393b434003270a45aec896f133492c855c77e6f08 UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -1024,7 +1056,7 @@ UNLIKELY_EOF fi else - # Phase 1: Upgrade certbot-auto if neceesary, then self-invoke. + # Phase 1: Upgrade certbot-auto if necessary, then self-invoke. # # Each phase checks the version of only the thing it is responsible for # upgrading. Phase 1 checks the version of the latest release of diff --git a/letsencrypt-auto-source/build.py b/letsencrypt-auto-source/build.py index ea74f9766..eebad61b7 100755 --- a/letsencrypt-auto-source/build.py +++ b/letsencrypt-auto-source/build.py @@ -8,26 +8,13 @@ other, special definitions. """ from os.path import abspath, dirname, join import re -from sys import argv + +from version import certbot_version, file_contents DIR = dirname(abspath(__file__)) -def certbot_version(build_script_dir): - """Return the version number stamped in certbot/__init__.py.""" - return re.search('''^__version__ = ['"](.+)['"].*''', - file_contents(join(dirname(build_script_dir), - 'certbot', - '__init__.py')), - re.M).group(1) - - -def file_contents(path): - with open(path) as file: - return file.read() - - def build(version=None, requirements=None): """Return the built contents of the letsencrypt-auto script. diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 4f3794505..baa5a7a9a 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 -iQEcBAABAgAGBQJYeWiuAAoJEE0XyZXNl3Xy4DsIALpCrYVDK9g1nQtdNzBJuFq7 -WlMEk3ofMrh+3sTEit1jr9+zJ2hV3POa3RtCRfRZsP0hsiNx7XbuR8t6yM6d4j3S -pAO0L5brrNoJKvKPx5v8AVGKm1EIDH/lWx/hH+HiE7OE3z/w0ppwoIy0/xIqMXt9 -O7Q70qV5Rvzh0K3KwSS9pb82ybV/mQ35uugravSOTMo2hZ82Ifh2ZEkJIsS4si2j -Oc26ruLi6ru4vwHJU2DkHZzl0oncyQZFolMZJmB47vNOCDtC303cR/poeQS9LSmF -vq3Lr0FAunCBoCjuyIGMk2SfmIhtJMS1v5dxOpZppVexedBYkoqU0FoUp/10/rM= -=7Xh1 +iQEcBAABAgAGBQJYkh+9AAoJEE0XyZXNl3XyDFcH/RvYdrkHaLNfc7HX6cRgZvkM +9XgP5j+Fsb44NP/U7FwVNHfgqe7OSSSHzNuXPt4/wZTRtXgQZjKU4fWlg8OTgNe8 +MvTx7FL78GXOhMPC0DX3ZkYYOllApiAFHQuwOXroGb099PTr7msnatLunLk1yCUN +wx/i+z0PHkJp+VDDb71sNOIwDtSzRx8w8/dtnnlODkDQWbbijkMUfslmEZnd5bRH +7vd2miuDkSR2dMdYLi+Zx0tkSib06kR8ahakPEIcDuZ5CI3fRESkmRUjOxFZOt/z +KqMVLMeEq0P81anlK3QCslwibhC88BlbafEma/FNPBZdzHz4UcQALWDGtYDn2tg= +=+aXk -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 453903ab2..bd519ff61 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -23,7 +23,7 @@ if [ -z "$VENV_PATH" ]; then VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" fi VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.11.0.dev0" +LE_AUTO_VERSION="0.12.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -105,7 +105,7 @@ fi # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but # this script *can* be run as root (not recommended), or fall back to using -# `su`. Auto-detection can be overrided by explicitly setting the +# `su`. Auto-detection can be overridden by explicitly setting the # environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below. # Because the parameters in `su -c` has to be a string, @@ -833,18 +833,18 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.10.1 \ - --hash=sha256:1dd5124078bc44739065409f3be51765608a90994c83460578c2680c582c1026 \ - --hash=sha256:f51c2fb0a31646364abeb7fdd8cfc7c8a4e63b0641b14ab3ce1b2e3a8921a211 -certbot==0.10.1 \ - --hash=sha256:9a56fc76f726beeed2f5a08d690088377cd430907f8a38c50e2aa9a258ee1253 \ - --hash=sha256:e0d699adb3f8ca3e077a4db339de29ebb3f790fbc5f3f02e446e227ed40aa743 -certbot-apache==0.10.1 \ - --hash=sha256:1252fd7e435ba48484b0bd9b72535a9755b03d8f0440f164b9c1c560d96cadb8 \ - --hash=sha256:134f46690da55262125defa58aa74472eb4a1555c9ed83edb3c8667df5a561b5 -certbot-nginx==0.10.1 \ - --hash=sha256:afd15ed9e4f3076056b63916f272b7287084d871cb8136477d16b08f64d514f0 \ - --hash=sha256:da1b7ea4831ead3f9eb526ee11bf1bf197da0fea4defeeb7b1ce24c5d3f45b51 +acme==0.11.0 \ + --hash=sha256:9c084f9a62241a11231af63266f2f12ad696be590393a4ab4974276a47d63404 \ + --hash=sha256:1513ae74ee8424c739a953a552890830315669d95d105469d1cff5b7db9ae888 +certbot==0.11.0 \ + --hash=sha256:cc94d890a8697b3bdbdc555bcf4ba93955f64324bc256b2ea710fd053fc03b8a \ + --hash=sha256:0f91fee360f9ce5e0584d0954fa3123832435f77f465915389032a90ac0248b1 +certbot-apache==0.11.0 \ + --hash=sha256:9a01883ca7e1159cff2a6e36bf97b83793c899c62944335f6ac2f59f9b3e8b5d \ + --hash=sha256:2b67871e7ae8bbfa7a2779fcd6444f28847fbb7a347ef4bfdb19fb55a0d75673 +certbot-nginx==0.11.0 \ + --hash=sha256:cfae45a42560e39889eebd287437556084c421a9e07a2deb3cbc0aeef92d2dab \ + --hash=sha256:dbddffe47c8e8b2d1cf47fe393b434003270a45aec896f133492c855c77e6f08 UNLIKELY_EOF # ------------------------------------------------------------------------- @@ -1056,7 +1056,7 @@ UNLIKELY_EOF fi else - # Phase 1: Upgrade certbot-auto if neceesary, then self-invoke. + # Phase 1: Upgrade certbot-auto if necessary, then self-invoke. # # Each phase checks the version of only the thing it is responsible for # upgrading. Phase 1 checks the version of the latest release of diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index b6ae7f614..3516f2b67 100644 Binary files a/letsencrypt-auto-source/letsencrypt-auto.sig and b/letsencrypt-auto-source/letsencrypt-auto.sig differ diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index a8a9f04d5..327799210 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -105,7 +105,7 @@ fi # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but # this script *can* be run as root (not recommended), or fall back to using -# `su`. Auto-detection can be overrided by explicitly setting the +# `su`. Auto-detection can be overridden by explicitly setting the # environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below. # Because the parameters in `su -c` has to be a string, @@ -355,7 +355,7 @@ UNLIKELY_EOF fi else - # Phase 1: Upgrade certbot-auto if neceesary, then self-invoke. + # Phase 1: Upgrade certbot-auto if necessary, then self-invoke. # # Each phase checks the version of only the thing it is responsible for # upgrading. Phase 1 checks the version of the latest release of diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index dc199bb4a..de0b56c7e 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -171,15 +171,15 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.10.1 \ - --hash=sha256:1dd5124078bc44739065409f3be51765608a90994c83460578c2680c582c1026 \ - --hash=sha256:f51c2fb0a31646364abeb7fdd8cfc7c8a4e63b0641b14ab3ce1b2e3a8921a211 -certbot==0.10.1 \ - --hash=sha256:9a56fc76f726beeed2f5a08d690088377cd430907f8a38c50e2aa9a258ee1253 \ - --hash=sha256:e0d699adb3f8ca3e077a4db339de29ebb3f790fbc5f3f02e446e227ed40aa743 -certbot-apache==0.10.1 \ - --hash=sha256:1252fd7e435ba48484b0bd9b72535a9755b03d8f0440f164b9c1c560d96cadb8 \ - --hash=sha256:134f46690da55262125defa58aa74472eb4a1555c9ed83edb3c8667df5a561b5 -certbot-nginx==0.10.1 \ - --hash=sha256:afd15ed9e4f3076056b63916f272b7287084d871cb8136477d16b08f64d514f0 \ - --hash=sha256:da1b7ea4831ead3f9eb526ee11bf1bf197da0fea4defeeb7b1ce24c5d3f45b51 +acme==0.11.0 \ + --hash=sha256:9c084f9a62241a11231af63266f2f12ad696be590393a4ab4974276a47d63404 \ + --hash=sha256:1513ae74ee8424c739a953a552890830315669d95d105469d1cff5b7db9ae888 +certbot==0.11.0 \ + --hash=sha256:cc94d890a8697b3bdbdc555bcf4ba93955f64324bc256b2ea710fd053fc03b8a \ + --hash=sha256:0f91fee360f9ce5e0584d0954fa3123832435f77f465915389032a90ac0248b1 +certbot-apache==0.11.0 \ + --hash=sha256:9a01883ca7e1159cff2a6e36bf97b83793c899c62944335f6ac2f59f9b3e8b5d \ + --hash=sha256:2b67871e7ae8bbfa7a2779fcd6444f28847fbb7a347ef4bfdb19fb55a0d75673 +certbot-nginx==0.11.0 \ + --hash=sha256:cfae45a42560e39889eebd287437556084c421a9e07a2deb3cbc0aeef92d2dab \ + --hash=sha256:dbddffe47c8e8b2d1cf47fe393b434003270a45aec896f133492c855c77e6f08 diff --git a/letsencrypt-auto-source/version.py b/letsencrypt-auto-source/version.py new file mode 100755 index 000000000..c49d96654 --- /dev/null +++ b/letsencrypt-auto-source/version.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +"""Get the current Certbot version number. + +Provides simple utilities for determining the Certbot version number and +building letsencrypt-auto. + +""" +from __future__ import print_function +from os.path import abspath, dirname, join +import re + + +def certbot_version(build_script_dir): + """Return the version number stamped in certbot/__init__.py.""" + return re.search('''^__version__ = ['"](.+)['"].*''', + file_contents(join(dirname(build_script_dir), + 'certbot', + '__init__.py')), + re.M).group(1) + + +def file_contents(path): + with open(path) as file: + return file.read() + + +if __name__ == '__main__': + print(certbot_version(dirname(abspath(__file__)))) diff --git a/setup.py b/setup.py index bc208f693..529459cf0 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,9 @@ changes = read_file(os.path.join(here, 'CHANGES.rst')) version = meta['version'] # Please update tox.ini when modifying dependency version requirements +# This package relies on requests, however, it isn't specified here to avoid +# masking the more specific request requirements in acme. See +# https://github.com/pypa/pip/issues/988 for more info. install_requires = [ 'acme=={0}'.format(version), # We technically need ConfigArgParse 0.10.0 for Python 2.6 support, but @@ -67,7 +70,6 @@ dev_extras = [ 'astroid==1.3.5', 'coverage', 'nose', - 'psutil>=2.2.1', # for tests, optional 'pylint==1.4.2', # upstream #248 'tox', 'twine', diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 9c6bc7708..4a2131006 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -47,23 +47,24 @@ common() { export HOOK_TEST="/tmp/hook$$" CheckHooks() { - COMMON="wtf2.auth\nwtf2.cleanup\nrenew\nrenew" EXPECTED="/tmp/expected$$" if [ $(head -n1 $HOOK_TEST) = "wtf.pre" ]; then echo "wtf.pre" > "$EXPECTED" echo "wtf2.pre" >> "$EXPECTED" - echo $COMMON >> "$EXPECTED" + echo "renew" >> "$EXPECTED" + echo "renew" >> "$EXPECTED" echo "wtf.post" >> "$EXPECTED" echo "wtf2.post" >> "$EXPECTED" else echo "wtf2.pre" > "$EXPECTED" echo "wtf.pre" >> "$EXPECTED" - echo $COMMON >> "$EXPECTED" + echo "renew" >> "$EXPECTED" + echo "renew" >> "$EXPECTED" echo "wtf2.post" >> "$EXPECTED" echo "wtf.post" >> "$EXPECTED" fi - if cmp --quiet "$EXPECTED" "$HOOK_TEST" ; then + if ! cmp --quiet "$EXPECTED" "$HOOK_TEST" ; then echo Hooks did not run as expected\; got cat "$HOOK_TEST" echo Expected @@ -91,12 +92,12 @@ common --domains le2.wtf --preferred-challenges http-01 run \ kill $python_server_pid common certonly -a manual -d le.wtf --rsa-key-size 4096 \ - --manual-auth-hook 'echo wtf2.auth >> "$HOOK_TEST" && ./tests/manual-http-auth.sh' \ - --manual-cleanup-hook 'echo wtf2.cleanup >> "$HOOK_TEST" && ./tests/manual-http-cleanup.sh' \ + --manual-auth-hook ./tests/manual-http-auth.sh \ + --manual-cleanup-hook ./tests/manual-http-cleanup.sh \ --pre-hook 'echo wtf2.pre >> "$HOOK_TEST"' \ --post-hook 'echo wtf2.post >> "$HOOK_TEST"' -common certonly -a manual -d dns.le.wtf --preferred-challenges dns-01 \ +common certonly -a manual -d dns.le.wtf --preferred-challenges dns,tls-sni \ --manual-auth-hook ./tests/manual-dns-auth.sh export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \ @@ -113,31 +114,32 @@ common --domains le3.wtf install \ --key-path "${root}/csr/key.pem" CheckCertCount() { - CERTCOUNT=`ls "${root}/conf/archive/le.wtf/cert"* | wc -l` - if [ "$CERTCOUNT" -ne "$1" ] ; then - echo Wrong cert count, not "$1" `ls "${root}/conf/archive/le.wtf/"*` + CERTCOUNT=`ls "${root}/conf/archive/$1/cert"* | wc -l` + if [ "$CERTCOUNT" -ne "$2" ] ; then + echo Wrong cert count, not "$2" `ls "${root}/conf/archive/$1/"*` exit 1 fi } -CheckCertCount 1 +CheckCertCount "le.wtf" 1 # This won't renew (because it's not time yet) common_no_force_renew renew -CheckCertCount 1 +CheckCertCount "le.wtf" 1 -# --renew-by-default is used, so renewal should occur -[ -f "$HOOK_TEST" ] && rm -f "$HOOK_TEST" -common renew -CheckCertCount 2 -CheckHooks +# renew using HTTP manual auth hooks +common renew --cert-name le.wtf --authenticator manual +CheckCertCount "le.wtf" 2 +# renew using DNS manual auth hooks +common renew --cert-name dns.le.wtf --authenticator manual +CheckCertCount "dns.le.wtf" 2 # This will renew because the expiry is less than 10 years from now sed -i "4arenew_before_expiry = 4 years" "$root/conf/renewal/le.wtf.conf" common_no_force_renew renew --rsa-key-size 2048 -CheckCertCount 3 +CheckCertCount "le.wtf" 3 -# The 4096 bit setting should persist to the first renewal, but be overriden in the second +# The 4096 bit setting should persist to the first renewal, but be overridden in the second size1=`wc -c ${root}/conf/archive/le.wtf/privkey1.pem | cut -d" " -f1` size2=`wc -c ${root}/conf/archive/le.wtf/privkey2.pem | cut -d" " -f1` @@ -149,6 +151,12 @@ if [ "$size1" -lt 3000 ] || [ "$size2" -lt 3000 ] || [ "$size3" -gt 1800 ] ; the exit 1 fi +# --renew-by-default is used, so renewal should occur +[ -f "$HOOK_TEST" ] && rm -f "$HOOK_TEST" +common renew +CheckCertCount "le.wtf" 4 +CheckHooks + # ECDSA openssl ecparam -genkey -name secp384r1 -out "${root}/privkey-p384.pem" SAN="DNS:ecdsa.le.wtf" openssl req -new -sha256 \ diff --git a/tests/letstest/scripts/test_leauto_upgrades.sh b/tests/letstest/scripts/test_leauto_upgrades.sh index 08247fe8d..f7a74821b 100755 --- a/tests/letstest/scripts/test_leauto_upgrades.sh +++ b/tests/letstest/scripts/test_leauto_upgrades.sh @@ -29,7 +29,8 @@ unset PIP_INDEX_URL export PIP_EXTRA_INDEX_URL="$SAVE" git checkout -f "$BRANCH" -if ! ./letsencrypt-auto -v --debug --version | grep 0.9.0 ; then +EXPECTED_VERSION=$(grep -m1 LE_AUTO_VERSION letsencrypt-auto | cut -d\" -f2) +if ! ./letsencrypt-auto -v --debug --version --no-self-upgrade | grep $EXPECTED_VERSION ; then echo upgrade appeared to fail exit 1 fi diff --git a/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh b/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh index 9b5ff88a2..7a86f8d9d 100755 --- a/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh +++ b/tests/letstest/scripts/test_letsencrypt_auto_certonly_standalone.sh @@ -21,9 +21,9 @@ letsencrypt-auto certonly --no-self-upgrade -v --standalone --debug \ # 1. be in the right directory cd tests/letstest/testdata/ -# 2. refer to the config with the same level of relativitity that it itself +# 2. refer to the config with the same level of relativity that it itself # contains :/ -OUT=`letsencrypt-auto certificates --config-dir sample-config -v` +OUT=`letsencrypt-auto certificates --config-dir sample-config -v --no-self-upgrade` TEST_CERTS=`echo "$OUT" | grep TEST_CERT | wc -l` REVOKED=`echo "$OUT" | grep REVOKED | wc -l` diff --git a/tests/letstest/scripts/test_ocsp_experimental.sh b/tests/letstest/scripts/test_ocsp_experimental.sh deleted file mode 100755 index cc787653c..000000000 --- a/tests/letstest/scripts/test_ocsp_experimental.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -x - -# $PUBLIC_IP $PRIVATE_IP $PUBLIC_HOSTNAME $BOULDER_URL are dynamically set at execution - -# with curl, instance metadata available from EC2 metadata service: -#public_host=$(curl -s http://169.254.169.254/2014-11-05/meta-data/public-hostname) -#public_ip=$(curl -s http://169.254.169.254/2014-11-05/meta-data/public-ipv4) -#private_ip=$(curl -s http://169.254.169.254/2014-11-05/meta-data/local-ipv4) - -cd letsencrypt -export PATH="$PWD/letsencrypt-auto-source:$PATH" -letsencrypt-auto-source/letsencrypt-auto --os-packages-only --debug --version -tools/venv.sh -sudo venv/bin/certbot certonly --no-self-upgrade -v --standalone --debug \ - --text --agree-dev-preview --agree-tos \ - --renew-by-default --redirect \ - --register-unsafely-without-email \ - --domain $PUBLIC_HOSTNAME --server $BOULDER_URL - -# we have to jump through some hoops to cope with relative paths in renewal -# conf files ... -# 1. be in the right directory -cd tests/letstest/testdata/ - -# 2. refer to the config with the same level of relativitity that it itself -# contains :/ -OUT=`sudo ../../../venv/bin/certbot certificates -v --config-dir sample-config` -TEST_CERTS=`echo "$OUT" | grep TEST_CERT | wc -l` -REVOKED=`echo "$OUT" | grep REVOKED | wc -l` - -if [ "$TEST_CERTS" != 2 ] ; then - echo "Did not find two test certs as expected ($TEST_CERTS)" - exit 1 -fi - -if [ "$REVOKED" != 1 ] ; then - echo "Did not find one revoked cert as expected ($REVOKED)" - exit 1 -fi diff --git a/tests/letstest/scripts/test_sdists.sh b/tests/letstest/scripts/test_sdists.sh new file mode 100755 index 000000000..e4ebd2e14 --- /dev/null +++ b/tests/letstest/scripts/test_sdists.sh @@ -0,0 +1,36 @@ +#!/bin/sh -xe + +cd letsencrypt +./certbot-auto --os-packages-only -n --debug + +PLUGINS="certbot-apache certbot-nginx" +PYTHON=$(command -v python2.7 || command -v python27 || command -v python2 || command -v python) +TEMP_DIR=$(mktemp -d) +VERSION=$(letsencrypt-auto-source/version.py) + +# setup venv +virtualenv --no-site-packages -p $PYTHON --setuptools venv +. ./venv/bin/activate +pip install -U pip +pip install -U setuptools + +# build sdists +for pkg_dir in acme . $PLUGINS; do + cd $pkg_dir + python setup.py clean + rm -rf build dist + python setup.py sdist + mv dist/* $TEMP_DIR + cd - +done + +# test sdists +cd $TEMP_DIR +for pkg in acme certbot $PLUGINS; do + tar -xvf "$pkg-$VERSION.tar.gz" + cd "$pkg-$VERSION" + python setup.py build + python setup.py test + python setup.py install + cd - +done diff --git a/tests/modification-check.sh b/tests/modification-check.sh index 73cdb0c09..5168f5ab5 100755 --- a/tests/modification-check.sh +++ b/tests/modification-check.sh @@ -46,6 +46,7 @@ cd ${SCRIPT_PATH}/../ cp letsencrypt-auto-source/letsencrypt-auto ${temp_dir}/original-lea python letsencrypt-auto-source/build.py cp letsencrypt-auto-source/letsencrypt-auto ${temp_dir}/build-lea +cp ${temp_dir}/original-lea letsencrypt-auto-source/letsencrypt-auto cd $temp_dir @@ -60,8 +61,8 @@ else build.py." fi +rm -rf $temp_dir + if $FLAG ; then exit 1 fi - -rm -rf temp_dir diff --git a/tools/_venv_common.sh b/tools/_venv_common.sh index a121af82d..ddbb02c62 100755 --- a/tools/_venv_common.sh +++ b/tools/_venv_common.sh @@ -2,7 +2,7 @@ VENV_NAME=${VENV_NAME:-venv} -# .egg-info directories tend to cause bizzaire problems (e.g. `pip -e +# .egg-info directories tend to cause bizarre problems (e.g. `pip -e # .` might unexpectedly install letshelp-certbot only, in case # `python letshelp-certbot/setup.py build` has been called # earlier) @@ -12,13 +12,13 @@ rm -rf *.egg-info # `/home/jakub/dev/letsencrypt/letsencrypt/venv/bin/python2` and # `venv/bin/python2` are the same file mv $VENV_NAME "$VENV_NAME.$(date +%s).bak" || true -virtualenv --no-site-packages $VENV_NAME $VENV_ARGS +virtualenv --no-site-packages --setuptools $VENV_NAME $VENV_ARGS . ./$VENV_NAME/bin/activate # Separately install setuptools and pip to make sure following # invocations use latest -pip install -U setuptools pip install -U pip +pip install -U setuptools pip install "$@" set +x diff --git a/tools/release.sh b/tools/release.sh index be306d8e0..75a4af29c 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -72,7 +72,7 @@ pip install -U virtualenv root_without_le="$version.$$" root="./releases/le.$root_without_le" -echo "Cloning into fresh copy at $root" # clean repo = no artificats +echo "Cloning into fresh copy at $root" # clean repo = no artifacts git clone . $root git rev-parse HEAD cd $root diff --git a/tox.ini b/tox.ini index 6f6903948..e6317e665 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ envlist = modification,py{26,33,34,35,36},cover,lint # packages installed separately to ensure that downstream deps problems # are detected, c.f. #1002 commands = - pip install -e acme[dns,dev] + pip install -e acme[dev] nosetests -v acme --processes=-1 pip install -e .[dev] nosetests -v certbot --processes=-1 --process-timeout=100 @@ -37,35 +37,33 @@ deps = py{26,27}-oldest: cffi<=1.7 py{26,27}-oldest: cryptography==0.8 py{26,27}-oldest: configargparse==0.10.0 - py{26,27}-oldest: dnspython>=1.12 - py{26,27}-oldest: psutil==2.1.0 py{26,27}-oldest: PyOpenSSL==0.13 py{26,27}-oldest: requests<=2.11.1 [testenv:py33] commands = - pip install -e acme[dns,dev] + pip install -e acme[dev] nosetests -v acme --processes=-1 pip install -e .[dev] nosetests -v certbot --processes=-1 --process-timeout=100 [testenv:py34] commands = - pip install -e acme[dns,dev] + pip install -e acme[dev] nosetests -v acme --processes=-1 pip install -e .[dev] nosetests -v certbot --processes=-1 --process-timeout=100 [testenv:py35] commands = - pip install -e acme[dns,dev] + pip install -e acme[dev] nosetests -v acme --processes=-1 pip install -e .[dev] nosetests -v certbot --processes=-1 --process-timeout=100 [testenv:py36] commands = - pip install -e acme[dns,dev] + pip install -e acme[dev] nosetests -v acme --processes=-1 pip install -e .[dev] nosetests -v certbot --processes=-1 --process-timeout=100 @@ -73,12 +71,12 @@ commands = [testenv:py27_install] basepython = python2.7 commands = - pip install -e acme[dns,dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot + pip install -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot [testenv:cover] basepython = python2.7 commands = - pip install -e acme[dns,dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot + pip install -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot ./tox.cover.sh [testenv:lint] @@ -88,7 +86,7 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - pip install -q -e acme[dns,dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot + pip install -q -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot pylint --reports=n --rcfile=.pylintrc acme/acme certbot certbot-apache/certbot_apache certbot-nginx/certbot_nginx certbot-compatibility-test/certbot_compatibility_test letshelp-certbot/letshelp_certbot [testenv:apacheconftest]