diff --git a/.travis.yml b/.travis.yml index 1fb618b0f..5af8908c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,10 +12,10 @@ before_install: - cp .travis.yml /tmp/travis.yml - git pull origin master --strategy=recursive --strategy-option=theirs --no-edit - if ! git diff .travis.yml /tmp/travis.yml ; then echo "Please merge master into test-everything"; exit 1; fi - - '[ "$TRAVIS_OS_NAME" != osx ] || tests/travis-macos-setup.sh' before_script: - 'if [ $TRAVIS_OS_NAME = osx ] ; then ulimit -n 1024 ; fi' + - export TOX_TESTENV_PASSENV=TRAVIS matrix: include: @@ -75,8 +75,9 @@ matrix: packages: # don't install nginx and apache - libaugeas0 - python: "2.7" - env: TOXENV=apacheconftest + env: TOXENV=apacheconftest-with-pebble sudo: required + services: docker - python: "3.4" env: TOXENV=py34 BOULDER_INTEGRATION=v1 sudo: required @@ -120,9 +121,19 @@ matrix: - language: generic env: TOXENV=py27 os: osx + addons: + homebrew: + packages: + - augeas + - python2 - language: generic env: TOXENV=py3 os: osx + addons: + homebrew: + packages: + - augeas + - python3 # Only build pushes to the master branch, PRs, and branches beginning with diff --git a/CHANGELOG.md b/CHANGELOG.md index a25890929..724820356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Certbot change log -Certbot adheres to [Semantic Versioning](http://semver.org/). +Certbot adheres to [Semantic Versioning](https://semver.org/). -## 0.29.0 - master +## 0.31.0 - master ### Added @@ -14,14 +14,102 @@ Certbot adheres to [Semantic Versioning](http://semver.org/). ### Fixed -* +* Fixed accessing josepy contents through acme.jose when the full acme.jose + path is used. Despite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was: +* acme + +More details about these changes can be found on our GitHub repo. + +## 0.30.0 - 2019-01-02 + +### Added + +* Added the `update_account` subcommand for account management commands. + +### Changed + +* Copied account management functionality from the `register` subcommand + to the `update_account` subcommand. +* Marked usage `register --update-registration` for deprecation and + removal in a future release. + +### Fixed + +* Older modules in the josepy library can now be accessed through acme.jose + like it could in previous versions of acme. This is only done to preserve + backwards compatibility and support for doing this with new modules in josepy + will not be added. Users of the acme library should switch to using josepy + directly if they haven't done so already. + +Despite us having broken lockstep, we are continuing to release new versions of +all Certbot components during releases for the time being, however, the only +package with changes other than its version number was: + +* acme + +More details about these changes can be found on our GitHub repo. + +## 0.29.1 - 2018-12-05 + +### Added + * +### Changed + +* + +### Fixed + +* The default work and log directories have been changed back to + /var/lib/letsencrypt and /var/log/letsencrypt respectively. + +Despite us having broken lockstep, we are continuing to release new versions of +all Certbot components during releases for the time being, however, the only +package with changes other than its version number was: + +* certbot + +More details about these changes can be found on our GitHub repo. + +## 0.29.0 - 2018-12-05 + +### Added + +* Noninteractive renewals with `certbot renew` (those not started from a + terminal) now randomly sleep 1-480 seconds before beginning work in + order to spread out load spikes on the server side. +* Added External Account Binding support in cli and acme library. + Command line arguments --eab-kid and --eab-hmac-key added. + +### Changed + +* Private key permissioning changes: Renewal preserves existing group mode + & gid of previous private key material. Private keys for new + lineages (i.e. new certs, not renewed) default to 0o600. + +### Fixed + +* Update code and dependencies to clean up Resource and Deprecation Warnings. +* Only depend on imgconverter extension for Sphinx >= 1.6 + +Despite us having broken lockstep, we are continuing to release new versions of +all Certbot components during releases for the time being, however, the only +package with changes other than its version number was: + +* acme +* certbot +* certbot-apache +* certbot-dns-cloudflare +* certbot-dns-digitalocean +* certbot-dns-google +* certbot-nginx + More details about these changes can be found on our GitHub repo: https://github.com/certbot/certbot/milestone/62?closed=1 diff --git a/acme/acme/__init__.py b/acme/acme/__init__.py index e8a0b16a8..bab5b40ce 100644 --- a/acme/acme/__init__.py +++ b/acme/acme/__init__.py @@ -10,3 +10,18 @@ supported version: `draft-ietf-acme-01`_. https://github.com/ietf-wg-acme/acme/tree/draft-ietf-acme-acme-01 """ +import sys + +# This code exists to keep backwards compatibility with people using acme.jose +# before it became the standalone josepy package. +# +# It is based on +# https://github.com/requests/requests/blob/1278ecdf71a312dc2268f3bfc0aabfab3c006dcf/requests/packages.py + +import josepy as jose + +for mod in list(sys.modules): + # This traversal is apparently necessary such that the identities are + # preserved (acme.jose.* is josepy.*) + if mod == 'josepy' or mod.startswith('josepy.'): + sys.modules['acme.' + mod.replace('josepy', 'jose', 1)] = sys.modules[mod] diff --git a/acme/acme/client.py b/acme/acme/client.py index adc8ad9e3..41338e17e 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -33,6 +33,7 @@ logger = logging.getLogger(__name__) # https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning if sys.version_info < (2, 7, 9): # pragma: no cover try: + # pylint: disable=no-member requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() # type: ignore except AttributeError: import urllib3.contrib.pyopenssl # pylint: disable=import-error @@ -199,22 +200,6 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes return datetime.datetime.now() + datetime.timedelta(seconds=seconds) - def poll(self, authzr): - """Poll Authorization Resource for status. - - :param authzr: Authorization Resource - :type authzr: `.AuthorizationResource` - - :returns: Updated Authorization Resource and HTTP response. - - :rtype: (`.AuthorizationResource`, `requests.Response`) - - """ - response = self.net.get(authzr.uri) - updated_authzr = self._authzr_from_response( - response, authzr.body.identifier, authzr.uri) - return updated_authzr, response - def _revoke(self, cert, rsn, url): """Revoke certificate. @@ -236,6 +221,7 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes raise errors.ClientError( 'Successful revocation must return HTTP OK status') + class Client(ClientBase): """ACME client for a v1 API. @@ -388,6 +374,22 @@ class Client(ClientBase): body=jose.ComparableX509(OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_ASN1, response.content))) + def poll(self, authzr): + """Poll Authorization Resource for status. + + :param authzr: Authorization Resource + :type authzr: `.AuthorizationResource` + + :returns: Updated Authorization Resource and HTTP response. + + :rtype: (`.AuthorizationResource`, `requests.Response`) + + """ + response = self.net.get(authzr.uri) + updated_authzr = self._authzr_from_response( + response, authzr.body.identifier, authzr.uri) + return updated_authzr, response + def poll_and_request_issuance( self, csr, authzrs, mintime=5, max_attempts=10): """Poll and request issuance. @@ -651,13 +653,29 @@ class ClientV2(ClientBase): body = messages.Order.from_json(response.json()) authorizations = [] for url in body.authorizations: - authorizations.append(self._authzr_from_response(self.net.get(url), uri=url)) + authorizations.append(self._authzr_from_response(self._post_as_get(url), uri=url)) return messages.OrderResource( body=body, uri=response.headers.get('Location'), authorizations=authorizations, csr_pem=csr_pem) + def poll(self, authzr): + """Poll Authorization Resource for status. + + :param authzr: Authorization Resource + :type authzr: `.AuthorizationResource` + + :returns: Updated Authorization Resource and HTTP response. + + :rtype: (`.AuthorizationResource`, `requests.Response`) + + """ + response = self._post_as_get(authzr.uri) + updated_authzr = self._authzr_from_response( + response, authzr.body.identifier, authzr.uri) + return updated_authzr, response + def poll_and_finalize(self, orderr, deadline=None): """Poll authorizations and finalize the order. @@ -681,7 +699,7 @@ class ClientV2(ClientBase): responses = [] for url in orderr.body.authorizations: while datetime.datetime.now() < deadline: - authzr = self._authzr_from_response(self.net.get(url), uri=url) + authzr = self._authzr_from_response(self._post_as_get(url), uri=url) if authzr.body.status != messages.STATUS_PENDING: responses.append(authzr) break @@ -716,12 +734,12 @@ class ClientV2(ClientBase): self._post(orderr.body.finalize, wrapped_csr) while datetime.datetime.now() < deadline: time.sleep(1) - response = self.net.get(orderr.uri) + response = self._post_as_get(orderr.uri) body = messages.Order.from_json(response.json()) if body.error is not None: raise errors.IssuanceError(body.error) if body.certificate is not None: - certificate_response = self.net.get(body.certificate, + certificate_response = self._post_as_get(body.certificate, content_type=DER_CONTENT_TYPE).text return orderr.update(body=body, fullchain_pem=certificate_response) raise errors.TimeoutError() @@ -739,6 +757,39 @@ class ClientV2(ClientBase): """ return self._revoke(cert, rsn, self.directory['revokeCert']) + def external_account_required(self): + """Checks if ACME server requires External Account Binding authentication.""" + if hasattr(self.directory, 'meta') and self.directory.meta.external_account_required: + return True + else: + return False + + def _post_as_get(self, *args, **kwargs): + """ + Send GET request using the POST-as-GET protocol if needed. + The request will be first issued using POST-as-GET for ACME v2. If the ACME CA servers do + not support this yet and return an error, request will be retried using GET. + For ACME v1, only GET request will be tried, as POST-as-GET is not supported. + :param args: + :param kwargs: + :return: + """ + if self.acme_version >= 2: + # We add an empty payload for POST-as-GET requests + new_args = args[:1] + (None,) + args[1:] + try: + return self._post(*new_args, **kwargs) # pylint: disable=star-args + except messages.Error as error: + if error.code == 'malformed': + logger.debug('Error during a POST-as-GET request, ' + 'your ACME CA may not support it:\n%s', error) + logger.debug('Retrying request with GET.') + else: # pragma: no cover + raise + + # If POST-as-GET is not supported yet, we use a GET instead. + return self.net.get(*args, **kwargs) + class BackwardsCompatibleClientV2(object): """ACME client wrapper that tends towards V2-style calls, but @@ -768,12 +819,7 @@ class BackwardsCompatibleClientV2(object): self.client = ClientV2(directory, net=net) def __getattr__(self, name): - if name in vars(self.client): - return getattr(self.client, name) - elif name in dir(ClientBase): - return getattr(self.client, name) - else: - raise AttributeError() + return getattr(self.client, name) def new_account_and_tos(self, regr, check_tos_cb=None): """Combined register and agree_tos for V1, new_account for V2 @@ -880,6 +926,15 @@ class BackwardsCompatibleClientV2(object): else: return 1 + def external_account_required(self): + """Checks if the server requires an external account for ACMEv2 servers. + + Always return False for ACMEv1 servers, as it doesn't use External Account Binding.""" + if self.acme_version == 1: + return False + else: + return self.client.external_account_required() + class ClientNetwork(object): # pylint: disable=too-many-instance-attributes """Wrapper around requests that signs POSTs for authentication. @@ -943,7 +998,7 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes :rtype: `josepy.JWS` """ - jobj = obj.json_dumps(indent=2).encode() + jobj = obj.json_dumps(indent=2).encode() if obj else b'' logger.debug('JWS payload:\n%s', jobj) kwargs = { "alg": self.alg, diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 2046d2377..b3d0f1921 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -1,4 +1,5 @@ """Tests for acme.client.""" +# pylint: disable=too-many-lines import copy import datetime import json @@ -283,6 +284,37 @@ class BackwardsCompatibleClientV2Test(ClientTestBase): client.update_registration(mock.sentinel.regr, None) mock_client().update_registration.assert_called_once_with(mock.sentinel.regr, None) + # newNonce present means it will pick acme_version 2 + def test_external_account_required_true(self): + self.response.json.return_value = messages.Directory({ + 'newNonce': 'http://letsencrypt-test.com/acme/new-nonce', + 'meta': messages.Directory.Meta(external_account_required=True), + }).to_json() + + client = self._init() + + self.assertTrue(client.external_account_required()) + + # newNonce present means it will pick acme_version 2 + def test_external_account_required_false(self): + self.response.json.return_value = messages.Directory({ + 'newNonce': 'http://letsencrypt-test.com/acme/new-nonce', + 'meta': messages.Directory.Meta(external_account_required=False), + }).to_json() + + client = self._init() + + self.assertFalse(client.external_account_required()) + + def test_external_account_required_false_v1(self): + self.response.json.return_value = messages.Directory({ + 'meta': messages.Directory.Meta(external_account_required=False), + }).to_json() + + client = self._init() + + self.assertFalse(client.external_account_required()) + class ClientTest(ClientTestBase): """Tests for acme.client.Client.""" @@ -665,7 +697,7 @@ class ClientTest(ClientTestBase): def test_revocation_payload(self): obj = messages.Revocation(certificate=self.certr.body, reason=self.rsn) self.assertTrue('reason' in obj.to_partial_json().keys()) - self.assertEquals(self.rsn, obj.to_partial_json()['reason']) + self.assertEqual(self.rsn, obj.to_partial_json()['reason']) def test_revoke_bad_status_raises_error(self): self.response.status_code = http_client.METHOD_NOT_ALLOWED @@ -675,6 +707,7 @@ class ClientTest(ClientTestBase): self.certr, self.rsn) + class ClientV2Test(ClientTestBase): """Tests for acme.client.ClientV2.""" @@ -730,9 +763,10 @@ class ClientV2Test(ClientTestBase): authz_response2 = self.response authz_response2.json.return_value = self.authz2.to_json() authz_response2.headers['Location'] = self.authzr2.uri - self.net.get.side_effect = (authz_response, authz_response2) - self.assertEqual(self.client.new_order(CSR_SAN_PEM), self.orderr) + with mock.patch('acme.client.ClientV2._post_as_get') as mock_post_as_get: + mock_post_as_get.side_effect = (authz_response, authz_response2) + self.assertEqual(self.client.new_order(CSR_SAN_PEM), self.orderr) @mock.patch('acme.client.datetime') def test_poll_and_finalize(self, mock_datetime): @@ -821,6 +855,47 @@ class ClientV2Test(ClientTestBase): self.response.json.return_value = self.regr.body.update( contact=()).to_json() + def test_external_account_required_true(self): + self.client.directory = messages.Directory({ + 'meta': messages.Directory.Meta(external_account_required=True) + }) + + self.assertTrue(self.client.external_account_required()) + + def test_external_account_required_false(self): + self.client.directory = messages.Directory({ + 'meta': messages.Directory.Meta(external_account_required=False) + }) + + self.assertFalse(self.client.external_account_required()) + + def test_external_account_required_default(self): + self.assertFalse(self.client.external_account_required()) + + def test_post_as_get(self): + with mock.patch('acme.client.ClientV2._authzr_from_response') as mock_client: + mock_client.return_value = self.authzr2 + + self.client.poll(self.authzr2) # pylint: disable=protected-access + + self.client.net.post.assert_called_once_with( + self.authzr2.uri, None, acme_version=2, + new_nonce_url='https://www.letsencrypt-demo.org/acme/new-nonce') + self.client.net.get.assert_not_called() + + class FakeError(messages.Error): # pylint: disable=too-many-ancestors + """Fake error to reproduce a malformed request ACME error""" + def __init__(self): # pylint: disable=super-init-not-called + pass + @property + def code(self): + return 'malformed' + self.client.net.post.side_effect = FakeError() + + self.client.poll(self.authzr2) # pylint: disable=protected-access + + self.client.net.get.assert_called_once_with(self.authzr2.uri) + class MockJSONDeSerializable(jose.JSONDeSerializable): # pylint: disable=missing-docstring @@ -876,7 +951,6 @@ class ClientNetworkTest(unittest.TestCase): self.assertEqual(jws.signature.combined.kid, u'acct-uri') self.assertEqual(jws.signature.combined.url, u'url') - def test_check_response_not_ok_jobj_no_error(self): self.response.ok = False self.response.json.return_value = {} @@ -1039,8 +1113,8 @@ class ClientNetworkTest(unittest.TestCase): # Requests Library Exceptions except requests.exceptions.ConnectionError as z: #pragma: no cover - self.assertTrue("('Connection aborted.', error(111, 'Connection refused'))" - == str(z) or "[WinError 10061]" in str(z)) + self.assertTrue("'Connection aborted.'" in str(z) or "[WinError 10061]" in str(z)) + class ClientNetworkWithMockedResponseTest(unittest.TestCase): """Tests for acme.client.ClientNetwork which mock out response.""" diff --git a/acme/acme/crypto_util_test.py b/acme/acme/crypto_util_test.py index 168489d86..44b245bbe 100644 --- a/acme/acme/crypto_util_test.py +++ b/acme/acme/crypto_util_test.py @@ -209,8 +209,8 @@ class MakeCSRTest(unittest.TestCase): # have a get_extensions() method, so we skip this test if the method # isn't available. if hasattr(csr, 'get_extensions'): - self.assertEquals(len(csr.get_extensions()), 1) - self.assertEquals(csr.get_extensions()[0].get_data(), + self.assertEqual(len(csr.get_extensions()), 1) + self.assertEqual(csr.get_extensions()[0].get_data(), OpenSSL.crypto.X509Extension( b'subjectAltName', critical=False, @@ -227,7 +227,7 @@ class MakeCSRTest(unittest.TestCase): # have a get_extensions() method, so we skip this test if the method # isn't available. if hasattr(csr, 'get_extensions'): - self.assertEquals(len(csr.get_extensions()), 2) + self.assertEqual(len(csr.get_extensions()), 2) # NOTE: Ideally we would filter by the TLS Feature OID, but # OpenSSL.crypto.X509Extension doesn't give us the extension's raw OID, # and the shortname field is just "UNDEF" diff --git a/acme/acme/jose_test.py b/acme/acme/jose_test.py new file mode 100644 index 000000000..340624a4f --- /dev/null +++ b/acme/acme/jose_test.py @@ -0,0 +1,53 @@ +"""Tests for acme.jose shim.""" +import importlib +import unittest + +class JoseTest(unittest.TestCase): + """Tests for acme.jose shim.""" + + def _test_it(self, submodule, attribute): + if submodule: + acme_jose_path = 'acme.jose.' + submodule + josepy_path = 'josepy.' + submodule + else: + acme_jose_path = 'acme.jose' + josepy_path = 'josepy' + acme_jose_mod = importlib.import_module(acme_jose_path) + josepy_mod = importlib.import_module(josepy_path) + + self.assertIs(acme_jose_mod, josepy_mod) + self.assertIs(getattr(acme_jose_mod, attribute), getattr(josepy_mod, attribute)) + + # We use the imports below with eval, but pylint doesn't + # understand that. + # pylint: disable=eval-used,unused-variable + import acme + import josepy + acme_jose_mod = eval(acme_jose_path) + josepy_mod = eval(josepy_path) + self.assertIs(acme_jose_mod, josepy_mod) + self.assertIs(getattr(acme_jose_mod, attribute), getattr(josepy_mod, attribute)) + + def test_top_level(self): + self._test_it('', 'RS512') + + def test_submodules(self): + # This test ensures that the modules in josepy that were + # available at the time it was moved into its own package are + # available under acme.jose. Backwards compatibility with new + # modules or testing code is not maintained. + mods_and_attrs = [('b64', 'b64decode',), + ('errors', 'Error',), + ('interfaces', 'JSONDeSerializable',), + ('json_util', 'Field',), + ('jwa', 'HS256',), + ('jwk', 'JWK',), + ('jws', 'JWS',), + ('util', 'ImmutableMap',),] + + for mod, attr in mods_and_attrs: + self._test_it(mod, attr) + + +if __name__ == '__main__': + unittest.main() # pragma: no cover diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 7e86b0c3b..4400a6c31 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -1,6 +1,7 @@ """ACME protocol messages.""" import collections import six +import json import josepy as jose @@ -8,6 +9,7 @@ from acme import challenges from acme import errors from acme import fields from acme import util +from acme import jws OLD_ERROR_PREFIX = "urn:acme:error:" ERROR_PREFIX = "urn:ietf:params:acme:error:" @@ -27,6 +29,7 @@ ERROR_CODES = { 'tls': 'The server experienced a TLS error during domain verification', 'unauthorized': 'The client lacks sufficient authorization', 'unknownHost': 'The server could not resolve a domain name', + 'externalAccountRequired': 'The server requires external account binding', } ERROR_TYPE_DESCRIPTIONS = dict( @@ -176,6 +179,7 @@ class Directory(jose.JSONDeSerializable): _terms_of_service_v2 = jose.Field('termsOfService', omitempty=True) website = jose.Field('website', omitempty=True) caa_identities = jose.Field('caaIdentities', omitempty=True) + external_account_required = jose.Field('externalAccountRequired', omitempty=True) def __init__(self, **kwargs): kwargs = dict((self._internal_name(k), v) for k, v in kwargs.items()) @@ -258,6 +262,24 @@ class ResourceBody(jose.JSONObjectWithFields): """ACME Resource Body.""" +class ExternalAccountBinding(object): + """ACME External Account Binding""" + + @classmethod + def from_data(cls, account_public_key, kid, hmac_key, directory): + """Create External Account Binding Resource from contact details, kid and hmac.""" + + key_json = json.dumps(account_public_key.to_partial_json()).encode() + decoded_hmac_key = jose.b64.b64decode(hmac_key) + url = directory["newAccount"] + + eab = jws.JWS.sign(key_json, jose.jwk.JWKOct(key=decoded_hmac_key), + jose.jwa.HS256, None, + url, kid) + + return eab.to_partial_json() + + class Registration(ResourceBody): """Registration Resource Body. @@ -275,12 +297,13 @@ class Registration(ResourceBody): status = jose.Field('status', omitempty=True) terms_of_service_agreed = jose.Field('termsOfServiceAgreed', omitempty=True) only_return_existing = jose.Field('onlyReturnExisting', omitempty=True) + external_account_binding = jose.Field('externalAccountBinding', omitempty=True) phone_prefix = 'tel:' email_prefix = 'mailto:' @classmethod - def from_data(cls, phone=None, email=None, **kwargs): + def from_data(cls, phone=None, email=None, external_account_binding=None, **kwargs): """Create registration resource from contact details.""" details = list(kwargs.pop('contact', ())) if phone is not None: @@ -288,6 +311,10 @@ class Registration(ResourceBody): if email is not None: details.extend([cls.email_prefix + mail for mail in email.split(',')]) kwargs['contact'] = tuple(details) + + if external_account_binding: + kwargs['external_account_binding'] = external_account_binding + return cls(**kwargs) def _filter_contact(self, prefix): diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index 876fbe825..7efaaa1a3 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -174,6 +174,24 @@ class DirectoryTest(unittest.TestCase): self.assertTrue(result) +class ExternalAccountBindingTest(unittest.TestCase): + def setUp(self): + from acme.messages import Directory + self.key = jose.jwk.JWKRSA(key=KEY.public_key()) + self.kid = "kid-for-testing" + self.hmac_key = "hmac-key-for-testing" + self.dir = Directory({ + 'newAccount': 'http://url/acme/new-account', + }) + + def test_from_data(self): + from acme.messages import ExternalAccountBinding + eab = ExternalAccountBinding.from_data(self.key, self.kid, self.hmac_key, self.dir) + + self.assertEqual(len(eab), 3) + self.assertEqual(sorted(eab.keys()), sorted(['protected', 'payload', 'signature'])) + + class RegistrationTest(unittest.TestCase): """Tests for acme.messages.Registration.""" @@ -205,6 +223,22 @@ class RegistrationTest(unittest.TestCase): 'mailto:admin@foo.com', )) + def test_new_registration_from_data_with_eab(self): + from acme.messages import NewRegistration, ExternalAccountBinding, Directory + key = jose.jwk.JWKRSA(key=KEY.public_key()) + kid = "kid-for-testing" + hmac_key = "hmac-key-for-testing" + directory = Directory({ + 'newAccount': 'http://url/acme/new-account', + }) + eab = ExternalAccountBinding.from_data(key, kid, hmac_key, directory) + reg = NewRegistration.from_data(email='admin@foo.com', external_account_binding=eab) + self.assertEqual(reg.contact, ( + 'mailto:admin@foo.com', + )) + self.assertEqual(sorted(reg.external_account_binding.keys()), + sorted(['protected', 'payload', 'signature'])) + def test_phones(self): self.assertEqual(('1234',), self.reg.phones) diff --git a/acme/setup.py b/acme/setup.py index ad70c2947..eac3974fa 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -3,21 +3,21 @@ from setuptools import find_packages from setuptools.command.test import test as TestCommand import sys -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ # load_pem_private/public_key (>=0.6) # rsa_recover_prime_factors (>=0.8) - 'cryptography>=0.8', + 'cryptography>=1.2.3', # formerly known as acme.jose: 'josepy>=1.0.0', # Connection.set_tlsext_host_name (>=0.13) 'mock', - 'PyOpenSSL>=0.13', + 'PyOpenSSL>=0.13.1', 'pyrfc3339', 'pytz', - 'requests[security]>=2.4.1', # security extras added in 2.4.1 + 'requests[security]>=2.6.0', # security extras added in 2.4.1 'requests-toolbelt>=0.3.0', 'setuptools', 'six>=1.9.0', # needed for python_2_unicode_compatible diff --git a/appveyor.yml b/appveyor.yml index 725ecfbff..2b6b82747 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,17 +1,9 @@ +image: Visual Studio 2015 + environment: matrix: - - FYI: Python 3.4 on Windows Server 2012 R2 - TOXENV: py34 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 - - FYI: Python 3.4 on Windows Server 2016 - TOXENV: py34 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - - FYI: Python 3.5 on Windows Server 2016 - TOXENV: py35 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - - FYI: Python 3.7 on Windows Server 2016 + code coverage - TOXENV: py37-cover - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + - TOXENV: py35 + - TOXENV: py37-cover branches: only: @@ -23,7 +15,6 @@ install: # Use Python 3.7 by default - "SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH%" # Check env - - "echo %APPVEYOR_BUILD_WORKER_IMAGE%" - "python --version" # Upgrade pip to avoid warnings - "python -m pip install --upgrade pip" @@ -33,6 +24,7 @@ install: build: off test_script: + - set TOX_TESTENV_PASSENV=APPVEYOR # Test env is set by TOXENV env variable - tox diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index f431b9dab..16de3a3d8 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -91,7 +91,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ - description = "Apache Web Server plugin - Beta" + description = "Apache Web Server plugin" OS_DEFAULTS = dict( server_root="/etc/apache2", diff --git a/certbot-apache/certbot_apache/tests/apache-conf-files/apache-conf-test b/certbot-apache/certbot_apache/tests/apache-conf-files/apache-conf-test index dcbba9d3e..4838a6eee 100755 --- a/certbot-apache/certbot_apache/tests/apache-conf-files/apache-conf-test +++ b/certbot-apache/certbot_apache/tests/apache-conf-files/apache-conf-test @@ -3,6 +3,11 @@ # A hackish script to see if the client is behaving as expected # with each of the "passing" conf files. +if [ -z "$SERVER" ]; then + echo "Please set SERVER to the ACME server's directory URL." + exit 1 +fi + export EA=/etc/apache2/ TESTDIR="`dirname $0`" cd $TESTDIR/passing @@ -56,13 +61,16 @@ if [ "$1" = --debian-modules ] ; then done fi +CERTBOT_CMD="sudo $(command -v certbot) --server $SERVER -vvvv" +CERTBOT_CMD="$CERTBOT_CMD --debug --apache --register-unsafely-without-email" +CERTBOT_CMD="$CERTBOT_CMD --agree-tos certonly -t --no-verify-ssl" FAILS=0 trap CleanupExit INT for f in *.conf ; do echo -n testing "$f"... Setup - RESULT=`echo c | sudo $(command -v certbot) -vvvv --debug --staging --apache --register-unsafely-without-email --agree-tos certonly -t 2>&1` + RESULT=`echo c | $CERTBOT_CMD 2>&1` if echo $RESULT | grep -Eq \("Which names would you like"\|"mod_macro is not yet"\) ; then echo passed else diff --git a/certbot-apache/certbot_apache/tests/autohsts_test.py b/certbot-apache/certbot_apache/tests/autohsts_test.py index c5d720dd3..bf92a13ff 100644 --- a/certbot-apache/certbot_apache/tests/autohsts_test.py +++ b/certbot-apache/certbot_apache/tests/autohsts_test.py @@ -64,12 +64,12 @@ class AutoHSTSTest(util.ApacheTest): self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"]) # Verify initial value - self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path), + self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path), initial_val) # Increase self.config.update_autohsts(mock.MagicMock()) # Verify increased value - self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path), + self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path), inc_val) self.assertTrue(mock_prepare.called) @@ -80,7 +80,7 @@ class AutoHSTSTest(util.ApacheTest): initial_val = maxage.format(constants.AUTOHSTS_STEPS[0]) self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"]) # Verify initial value - self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path), + self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path), initial_val) self.config.update_autohsts(mock.MagicMock()) @@ -112,19 +112,19 @@ class AutoHSTSTest(util.ApacheTest): for i in range(len(constants.AUTOHSTS_STEPS)-1): # Ensure that value is not made permanent prematurely self.config.deploy_autohsts(mock_lineage) - self.assertNotEquals(self.get_autohsts_value(self.vh_truth[7].path), + self.assertNotEqual(self.get_autohsts_value(self.vh_truth[7].path), max_val) self.config.update_autohsts(mock.MagicMock()) # Value should match pre-permanent increment step cur_val = maxage.format(constants.AUTOHSTS_STEPS[i+1]) - self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path), + self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path), cur_val) # Ensure that the value is raised to max - self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path), + self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path), maxage.format(constants.AUTOHSTS_STEPS[-1])) # Make permanent self.config.deploy_autohsts(mock_lineage) - self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path), + self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path), max_val) def test_autohsts_update_noop(self): @@ -156,7 +156,7 @@ class AutoHSTSTest(util.ApacheTest): mock_id.return_value = "1234567" self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com", "ocspvhost.com"]) - self.assertEquals(mock_id.call_count, 1) + self.assertEqual(mock_id.call_count, 1) def test_autohsts_remove_orphaned(self): # pylint: disable=protected-access diff --git a/certbot-apache/certbot_apache/tests/centos_test.py b/certbot-apache/certbot_apache/tests/centos_test.py index 46b857c3e..a27916c32 100644 --- a/certbot-apache/certbot_apache/tests/centos_test.py +++ b/certbot-apache/certbot_apache/tests/centos_test.py @@ -81,9 +81,9 @@ class MultipleVhostsTestCentOS(util.ApacheTest): mock_osi.return_value = ("centos", "7") self.config.parser.update_runtime_variables() - self.assertEquals(mock_get.call_count, 3) - self.assertEquals(len(self.config.parser.modules), 4) - self.assertEquals(len(self.config.parser.variables), 2) + self.assertEqual(mock_get.call_count, 3) + self.assertEqual(len(self.config.parser.modules), 4) + self.assertEqual(len(self.config.parser.variables), 2) self.assertTrue("TEST2" in self.config.parser.variables.keys()) self.assertTrue("mod_another.c" in self.config.parser.modules) @@ -127,7 +127,7 @@ class MultipleVhostsTestCentOS(util.ApacheTest): def test_alt_restart_works(self, mock_run_script): mock_run_script.side_effect = [None, errors.SubprocessError, None] self.config.restart() - self.assertEquals(mock_run_script.call_count, 3) + self.assertEqual(mock_run_script.call_count, 3) @mock.patch("certbot_apache.configurator.util.run_script") def test_alt_restart_errors(self, mock_run_script): diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 0fb89c95a..a77dbf637 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -1402,11 +1402,11 @@ class MultipleVhostsTest(util.ApacheTest): vhs = self.config._choose_vhosts_wildcard("*.certbot.demo", create_ssl=True) # Check that the dialog was called with one vh: certbot.demo - self.assertEquals(mock_select_vhs.call_args[0][0][0], self.vh_truth[3]) - self.assertEquals(len(mock_select_vhs.call_args_list), 1) + self.assertEqual(mock_select_vhs.call_args[0][0][0], self.vh_truth[3]) + self.assertEqual(len(mock_select_vhs.call_args_list), 1) # And the actual returned values - self.assertEquals(len(vhs), 1) + self.assertEqual(len(vhs), 1) self.assertTrue(vhs[0].name == "certbot.demo") self.assertTrue(vhs[0].ssl) @@ -1421,7 +1421,7 @@ class MultipleVhostsTest(util.ApacheTest): vhs = self.config._choose_vhosts_wildcard("*.certbot.demo", create_ssl=False) self.assertFalse(mock_makessl.called) - self.assertEquals(vhs[0], self.vh_truth[1]) + self.assertEqual(vhs[0], self.vh_truth[1]) @mock.patch("certbot_apache.configurator.ApacheConfigurator._vhosts_for_wildcard") @mock.patch("certbot_apache.configurator.ApacheConfigurator.make_vhost_ssl") @@ -1434,15 +1434,15 @@ class MultipleVhostsTest(util.ApacheTest): mock_select_vhs.return_value = [self.vh_truth[7]] vhs = self.config._choose_vhosts_wildcard("whatever", create_ssl=True) - self.assertEquals(mock_select_vhs.call_args[0][0][0], self.vh_truth[7]) - self.assertEquals(len(mock_select_vhs.call_args_list), 1) + self.assertEqual(mock_select_vhs.call_args[0][0][0], self.vh_truth[7]) + self.assertEqual(len(mock_select_vhs.call_args_list), 1) # Ensure that make_vhost_ssl was not called, vhost.ssl == true self.assertFalse(mock_makessl.called) # And the actual returned values - self.assertEquals(len(vhs), 1) + self.assertEqual(len(vhs), 1) self.assertTrue(vhs[0].ssl) - self.assertEquals(vhs[0], self.vh_truth[7]) + self.assertEqual(vhs[0], self.vh_truth[7]) def test_deploy_cert_wildcard(self): @@ -1455,7 +1455,7 @@ class MultipleVhostsTest(util.ApacheTest): self.config.deploy_cert("*.wildcard.example.org", "/tmp/path", "/tmp/path", "/tmp/path", "/tmp/path") self.assertTrue(mock_dep.called) - self.assertEquals(len(mock_dep.call_args_list), 1) + self.assertEqual(len(mock_dep.call_args_list), 1) self.assertEqual(self.vh_truth[7], mock_dep.call_args_list[0][0][0]) @mock.patch("certbot_apache.display_ops.select_vhost_multiple") @@ -1651,7 +1651,8 @@ class MultiVhostsTest(util.ApacheTest): self.assertTrue(self.config.parser.find_dir( "RewriteEngine", "on", ssl_vhost.path, False)) - conf_text = open(ssl_vhost.filep).read() + with open(ssl_vhost.filep) as the_file: + conf_text = the_file.read() commented_rewrite_rule = ("# RewriteRule \"^/secrets/(.+)\" " "\"https://new.example.com/docs/$1\" [R,L]") uncommented_rewrite_rule = ("RewriteRule \"^/docs/(.+)\" " @@ -1667,7 +1668,8 @@ class MultiVhostsTest(util.ApacheTest): ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[3]) - conf_lines = open(ssl_vhost.filep).readlines() + with open(ssl_vhost.filep) as the_file: + conf_lines = the_file.readlines() conf_line_set = [l.strip() for l in conf_lines] not_commented_cond1 = ("RewriteCond " "%{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f") diff --git a/certbot-apache/certbot_apache/tests/gentoo_test.py b/certbot-apache/certbot_apache/tests/gentoo_test.py index 0681e30b5..f09d742a4 100644 --- a/certbot-apache/certbot_apache/tests/gentoo_test.py +++ b/certbot-apache/certbot_apache/tests/gentoo_test.py @@ -121,15 +121,15 @@ class MultipleVhostsTestGentoo(util.ApacheTest): mock_osi.return_value = ("gentoo", "123") self.config.parser.update_runtime_variables() - self.assertEquals(mock_get.call_count, 1) - self.assertEquals(len(self.config.parser.modules), 4) + self.assertEqual(mock_get.call_count, 1) + self.assertEqual(len(self.config.parser.modules), 4) self.assertTrue("mod_another.c" in self.config.parser.modules) @mock.patch("certbot_apache.configurator.util.run_script") def test_alt_restart_works(self, mock_run_script): mock_run_script.side_effect = [None, errors.SubprocessError, None] self.config.restart() - self.assertEquals(mock_run_script.call_count, 3) + self.assertEqual(mock_run_script.call_count, 3) if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/parser_test.py b/certbot-apache/certbot_apache/tests/parser_test.py index d62fd54e8..a089ec471 100644 --- a/certbot-apache/certbot_apache/tests/parser_test.py +++ b/certbot-apache/certbot_apache/tests/parser_test.py @@ -84,7 +84,7 @@ class BasicParserTest(util.ParserTest): self.assertEqual(self.parser.aug.get(match), str(i + 1)) def test_empty_arg(self): - self.assertEquals(None, + self.assertEqual(None, self.parser.get_arg("/files/whatever/nonexistent")) def test_add_dir_to_ifmodssl(self): @@ -303,7 +303,7 @@ class BasicParserTest(util.ParserTest): from certbot_apache.parser import get_aug_path self.parser.add_comment(get_aug_path(self.parser.loc["name"]), "123456") comm = self.parser.find_comments("123456") - self.assertEquals(len(comm), 1) + self.assertEqual(len(comm), 1) self.assertTrue(self.parser.loc["name"] in comm[0]) diff --git a/certbot-apache/local-oldest-requirements.txt b/certbot-apache/local-oldest-requirements.txt index e70ac0c7f..fd8869f7c 100644 --- a/certbot-apache/local-oldest-requirements.txt +++ b/certbot-apache/local-oldest-requirements.txt @@ -1,2 +1,2 @@ acme[dev]==0.25.0 --e .[dev] +certbot[dev]==0.26.0 diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index e6f6f1e23..14d6cacb6 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-auto b/certbot-auto index fe87317a7..be2c3679b 100755 --- a/certbot-auto +++ b/certbot-auto @@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then fi VENV_BIN="$VENV_PATH/bin" BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt" -LE_AUTO_VERSION="0.28.0" +LE_AUTO_VERSION="0.30.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -593,8 +593,7 @@ BootstrapArchCommon() { # - ArchLinux (x86_64) # # "python-virtualenv" is Python3, but "python2-virtualenv" provides - # only "virtualenv2" binary, not "virtualenv" necessary in - # ./tools/_venv_common.sh + # only "virtualenv2" binary, not "virtualenv". deps=" python2 @@ -912,6 +911,35 @@ OldVenvExists() { [ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ] } +# Given python path, version 1 and version 2, check if version 1 is outdated compared to version 2. +# An unofficial version provided as version 1 (eg. 0.28.0.dev0) will be treated +# specifically by printing "UNOFFICIAL". Otherwise, print "OUTDATED" if version 1 +# is outdated, and "UP_TO_DATE" if not. +# This function relies only on installed python environment (2.x or 3.x) by certbot-auto. +CompareVersions() { + "$1" - "$2" "$3" << "UNLIKELY_EOF" +import sys +from distutils.version import StrictVersion + +try: + current = StrictVersion(sys.argv[1]) +except ValueError: + sys.stdout.write('UNOFFICIAL') + sys.exit() + +try: + remote = StrictVersion(sys.argv[2]) +except ValueError: + sys.stdout.write('UP_TO_DATE') + sys.exit() + +if current < remote: + sys.stdout.write('OUTDATED') +else: + sys.stdout.write('UP_TO_DATE') +UNLIKELY_EOF +} + if [ "$1" = "--le-auto-phase2" ]; then # Phase 2: Create venv, install LE, and run. @@ -1017,43 +1045,39 @@ pycparser==2.14 \ asn1crypto==0.22.0 \ --hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \ --hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a -cffi==1.10.0 \ - --hash=sha256:446699c10f3c390633d0722bc19edbc7ac4b94761918a4a4f7908a24e86ebbd0 \ - --hash=sha256:562326fc7f55a59ef3fef5e82908fe938cdc4bbda32d734c424c7cd9ed73e93a \ - --hash=sha256:7f732ad4a30db0b39400c3f7011249f7d0701007d511bf09604729aea222871f \ - --hash=sha256:94fb8410c6c4fc48e7ea759d3d1d9ca561171a88d00faddd4aa0306f698ad6a0 \ - --hash=sha256:587a5043df4b00a2130e09fed42da02a4ed3c688bd9bf07a3ac89d2271f4fb07 \ - --hash=sha256:ec08b88bef627ec1cea210e1608c85d3cf44893bcde74e41b7f7dbdfd2c1bad6 \ - --hash=sha256:a41406f6d62abcdf3eef9fd998d8dcff04fd2a7746644143045feeebd76352d1 \ - --hash=sha256:b560916546b2f209d74b82bdbc3223cee9a165b0242fa00a06dfc48a2054864a \ - --hash=sha256:e74896774e437f4715c57edeb5cf3d3a40d7727f541c2c12156617b5a15d1829 \ - --hash=sha256:9a31c18ba4881a116e448c52f3f5d3e14401cf7a9c43cc88f06f2a7f5428da0e \ - --hash=sha256:80796ea68e11624a0279d3b802f88a7fe7214122b97a15a6c97189934a2cc776 \ - --hash=sha256:f4019826a2dec066c909a1f483ef0dcf9325d6740cc0bd15308942b28b0930f7 \ - --hash=sha256:7248506981eeba23888b4140a69a53c4c0c0a386abcdca61ed8dd790a73e64b9 \ - --hash=sha256:a8955265d146e86fe2ce116394be4eaf0cb40314a79b19f11c4fa574cd639572 \ - --hash=sha256:c49187260043bd4c1d6a52186f9774f17d9b1da0a406798ebf4bfc12da166ade \ - --hash=sha256:c1d8b3d8dcb5c23ac1a8bf56422036f3f305a3c5a8bc8c354256579a1e2aa2c1 \ - --hash=sha256:9e389615bcecb8c782a87939d752340bb0a3a097e90bae54d7f0915bc12f45bd \ - --hash=sha256:d09ff358f75a874f69fa7d1c2b4acecf4282a950293fcfcf89aa606da8a9a500 \ - --hash=sha256:b69b4557aae7de18b7c174a917fe19873529d927ac592762d9771661875bbd40 \ - --hash=sha256:5de52b081a2775e76b971de9d997d85c4457fc0a09079e12d66849548ae60981 \ - --hash=sha256:e7d88fecb7b6250a1fd432e6dc64890342c372fce13dbfe4bb6f16348ad00c14 \ - --hash=sha256:1426e67e855ef7f5030c9184f4f1a9f4bfa020c31c962cd41fd129ec5aef4a6a \ - --hash=sha256:267dd2c66a5760c5f4d47e2ebcf8eeac7ef01e1ae6ae7a6d0d241a290068bc38 \ - --hash=sha256:e553eb489511cacf19eda6e52bc9e151316f0d721724997dda2c4d3079b778db \ - --hash=sha256:98b89b2c57f97ce2db7aeba60db173c84871d73b40e41a11ea95de1500ddc57e \ - --hash=sha256:e2b7e090188833bc58b2ae03fb864c22688654ebd2096bcf38bc860c4f38a3d8 \ - --hash=sha256:afa7d8b8d38ad40db8713ee053d41b36d87d6ae5ec5ad36f9210b548a18dc214 \ - --hash=sha256:4fc9c2ff7924b3a1fa326e1799e5dd58cac585d7fb25fe53ccaa1333b0453d65 \ - --hash=sha256:937db39a1ec5af3003b16357b2042bba67c88d43bc11aaa203fa8a5924524209 \ - --hash=sha256:ab22285797631df3b513b2cd3ecdc51cd8e3d36788e3991d93d0759d6883b027 \ - --hash=sha256:96e599b924ef009aa867f725b3249ee51d76489f484d3a45b4bd219c5ec6ed59 \ - --hash=sha256:bea842a0512be6a8007e585790bccd5d530520fc025ce63b03e139be373b0063 \ - --hash=sha256:e7175287f7fe7b1cc203bb958b17db40abd732690c1e18e700f10e0843a58598 \ - --hash=sha256:285ab352552f52f1398c912556d4d36d4ea9b8450e5c65d03809bf9886755533 \ - --hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \ - --hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5 +cffi==1.11.5 \ + --hash=sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50 \ + --hash=sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596 \ + --hash=sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef \ + --hash=sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743 \ + --hash=sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f \ + --hash=sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31 \ + --hash=sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04 \ + --hash=sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6 \ + --hash=sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3 \ + --hash=sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6 \ + --hash=sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b \ + --hash=sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca \ + --hash=sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e \ + --hash=sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb \ + --hash=sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd \ + --hash=sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1 \ + --hash=sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917 \ + --hash=sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359 \ + --hash=sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f \ + --hash=sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95 \ + --hash=sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801 \ + --hash=sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257 \ + --hash=sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184 \ + --hash=sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc \ + --hash=sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085 \ + --hash=sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93 \ + --hash=sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2 \ + --hash=sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30 \ + --hash=sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5 \ + --hash=sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e \ + --hash=sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b \ + --hash=sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4 ConfigArgParse==0.12.0 \ --hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339 \ --no-binary ConfigArgParse @@ -1146,9 +1170,9 @@ pytz==2015.7 \ --hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \ --hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \ --hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3 -requests==2.12.1 \ - --hash=sha256:3f3f27a9d0f9092935efc78054ef324eb9f8166718270aefe036dfa1e4f68e1e \ - --hash=sha256:2109ecea94df90980be040490ff1d879971b024861539abb00054062388b612e +requests==2.20.0 \ + --hash=sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c \ + --hash=sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279 six==1.10.0 \ --hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \ --hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a @@ -1185,6 +1209,15 @@ zope.interface==4.1.3 \ requests-toolbelt==0.8.0 \ --hash=sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237 \ --hash=sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5 +chardet==3.0.2 \ + --hash=sha256:4f7832e7c583348a9eddd927ee8514b3bf717c061f57b21dbe7697211454d9bb \ + --hash=sha256:6ebf56457934fdce01fb5ada5582762a84eed94cad43ed877964aebbdd8174c0 +urllib3==1.24.1 \ + --hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \ + --hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22 +certifi==2017.4.17 \ + --hash=sha256:f4318671072f030a33c7ca6acaef720ddd50ff124d1388e50c1bda4cbd6d7010 \ + --hash=sha256:f7527ebf7461582ce95f7a9e03dd141ce810d40590834f4ec20cddd54234c10a # Contains the requirements for the letsencrypt package. # @@ -1197,31 +1230,29 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.28.0 \ - --hash=sha256:f2f7c816acd695cbcda713a779b0db2b08e9de407146b46e1c4ef5561e0f5417 \ - --hash=sha256:31e3e2ee2a25c009a621c59ac9182f85d937a897c7bd1d47d0e01f3c712a090a -acme==0.28.0 \ - --hash=sha256:d3a564031155fece3f6edce8c5246d4cf34be0977b4c7c5ce84e86c68601c895 \ - --hash=sha256:bf7c2f1c24a26ab5b9fce3a6abca1d74a5914d46919649ae00ad5817db62bb85 -certbot-apache==0.28.0 \ - --hash=sha256:a57d7bac4f13ae5ecea4f4cbd479d381c02316c4832b25ab5a9d7c6826166370 \ - --hash=sha256:3f93f5de4a548e973c493a6cac5eeeb3dbbcae2988b61299ea0727d04a00f5bb -certbot-nginx==0.28.0 \ - --hash=sha256:1822a65910f0801087fa20a3af3fc94f878f93e0f11809483bb5387df861e296 \ - --hash=sha256:426fb403b0a7b203629f4e350a862cbc3bc1f69936fdab8ec7eafe0d8a3b5ddb +certbot==0.30.0 \ + --hash=sha256:b3468e128e74d2295598f6d3fbf9d0edfb67fe5abaca3b985a9e858395bd027f \ + --hash=sha256:d631fe6c75700ce9b2fdae194ff8b53c7518545d87dd451a1704f7572dcd49e8 +acme==0.30.0 \ + --hash=sha256:eed9389f802ebf4988c9e43c28ad3d5c2734237371d78e97450a1d61189a15aa \ + --hash=sha256:984b6d00bec73dcfa616636a760e80ca14bd246fb908710a656547f542f09445 +certbot-apache==0.30.0 \ + --hash=sha256:d38c70fc6930db298ea992a3145362eebdce460d3d2651f86a8f2f43d838c6d0 \ + --hash=sha256:1d4bc207d53a3e5d37e5d9ebd05f26089aa21d1fbf384113ed9d1829b4d1e9bf +certbot-nginx==0.30.0 \ + --hash=sha256:6163c7d0080f59b4ebe510afcc6af2d2eebf15469275c3835866690db4d465d6 \ + --hash=sha256:e39a3f3d77cd4c653949cf066fb2211039fd2032665697c27b6e8501c7c2dd92 UNLIKELY_EOF # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/pipstrap.py" #!/usr/bin/env python """A small script that can act as a trust root for installing pip >=8 - Embed this in your project, and your VCS checkout is all you have to trust. In a post-peep era, this lets you claw your way to a hash-checking version of pip, with which you can install the rest of your dependencies safely. All it assumes is Python 2.6 or better and *some* version of pip already installed. If anything goes wrong, it will exit with a non-zero status code. - """ # This is here so embedded copies are MIT-compliant: # Copyright (c) 2016 Erik Rose @@ -1348,10 +1379,8 @@ def hashed_download(url, temp, digest): def get_index_base(): """Return the URL to the dir containing the "packages" folder. - Try to wring something out of PIP_INDEX_URL, if set. Hack "/simple" off the end if it's there; that is likely to give us the right dir. - """ env_var = environ.get('PIP_INDEX_URL', '').rstrip('/') if env_var: @@ -1641,7 +1670,12 @@ UNLIKELY_EOF error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates." elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then error "WARNING: unable to check for updates." - elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then + fi + + LE_VERSION_STATE=`CompareVersions "$LE_PYTHON" "$LE_AUTO_VERSION" "$REMOTE_VERSION"` + if [ "$LE_VERSION_STATE" = "UNOFFICIAL" ]; then + say "Unofficial certbot-auto version detected, self-upgrade is disabled: $LE_AUTO_VERSION" + elif [ "$LE_VERSION_STATE" = "OUTDATED" ]; then say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." # Now we drop into Python so we don't have to install even more diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index bfbbe0625..f519ed422 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.29.0.dev0' +version = '0.31.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py b/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py index f1156642f..7604a8baf 100644 --- a/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py +++ b/certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py @@ -122,7 +122,7 @@ class _CloudflareClient(object): self.cf.zones.dns_records.delete(zone_id, record_id) logger.debug('Successfully deleted TXT record.') except CloudFlare.exceptions.CloudFlareAPIError as e: - logger.warn('Encountered CloudFlareAPIError deleting TXT record: %s', e) + logger.warning('Encountered CloudFlareAPIError deleting TXT record: %s', e) else: logger.debug('TXT record not found; no cleanup needed.') else: diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index d615fa999..ff33293fe 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index f9880270a..5c8709445 100644 --- a/certbot-dns-cloudxns/setup.py +++ b/certbot-dns-cloudxns/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean.py b/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean.py index 4bf279279..5a4f22327 100644 --- a/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean.py +++ b/certbot-dns-digitalocean/certbot_dns_digitalocean/dns_digitalocean.py @@ -134,7 +134,7 @@ class _DigitalOceanClient(object): logger.debug('Removing TXT record with id: %s', record.id) record.destroy() except digitalocean.Error as e: - logger.warn('Error deleting TXT record %s using the DigitalOcean API: %s', + logger.warning('Error deleting TXT record %s using the DigitalOcean API: %s', record.id, e) def _find_domain(self, domain_name): diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index a9d46d128..2f7fa37d6 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index ac7bb1090..3bb43cf5d 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index ab944d40d..599cec486 100644 --- a/certbot-dns-dnsmadeeasy/setup.py +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-gehirn/setup.py b/certbot-dns-gehirn/setup.py index 94ca74761..894d809ac 100644 --- a/certbot-dns-gehirn/setup.py +++ b/certbot-dns-gehirn/setup.py @@ -1,10 +1,8 @@ -import sys - from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-google/certbot_dns_google/__init__.py b/certbot-dns-google/certbot_dns_google/__init__.py index f19266737..b88260b07 100644 --- a/certbot-dns-google/certbot_dns_google/__init__.py +++ b/certbot-dns-google/certbot_dns_google/__init__.py @@ -98,7 +98,7 @@ Examples certbot certonly \\ --dns-google \\ - --dns-google-credentials ~/.secrets/certbot/google.ini \\ + --dns-google-credentials ~/.secrets/certbot/google.json \\ --dns-google-propagation-seconds 120 \\ -d example.com diff --git a/certbot-dns-google/certbot_dns_google/dns_google.py b/certbot-dns-google/certbot_dns_google/dns_google.py index c204cb0ca..0b84dddb0 100644 --- a/certbot-dns-google/certbot_dns_google/dns_google.py +++ b/certbot-dns-google/certbot_dns_google/dns_google.py @@ -179,7 +179,7 @@ class _GoogleClient(object): try: zone_id = self._find_managed_zone_id(domain) except errors.PluginError as e: - logger.warn('Error finding zone. Skipping cleanup.') + logger.warning('Error finding zone. Skipping cleanup.') return record_contents = self.get_existing_txt_rrset(zone_id, record_name) @@ -219,7 +219,7 @@ class _GoogleClient(object): request = changes.create(project=self.project_id, managedZone=zone_id, body=data) request.execute() except googleapiclient_errors.Error as e: - logger.warn('Encountered error deleting TXT record: %s', e) + logger.warning('Encountered error deleting TXT record: %s', e) def get_existing_txt_rrset(self, zone_id, record_name): """ diff --git a/certbot-dns-google/certbot_dns_google/dns_google_test.py b/certbot-dns-google/certbot_dns_google/dns_google_test.py index b6f6e08b6..2b081885b 100644 --- a/certbot-dns-google/certbot_dns_google/dns_google_test.py +++ b/certbot-dns-google/certbot_dns_google/dns_google_test.py @@ -276,9 +276,9 @@ class GoogleClientTest(unittest.TestCase): [{'managedZones': [{'id': self.zone}]}]) # Record name mocked in setUp found = client.get_existing_txt_rrset(self.zone, "_acme-challenge.example.org") - self.assertEquals(found, ["\"example-txt-contents\""]) + self.assertEqual(found, ["\"example-txt-contents\""]) not_found = client.get_existing_txt_rrset(self.zone, "nonexistent.tld") - self.assertEquals(not_found, None) + self.assertEqual(not_found, None) @mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name') @mock.patch('certbot_dns_google.dns_google.open', diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index aa1afdc93..c99ad38aa 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-linode/setup.py b/certbot-dns-linode/setup.py index 4c2571f96..588b6a40a 100644 --- a/certbot-dns-linode/setup.py +++ b/certbot-dns-linode/setup.py @@ -1,9 +1,7 @@ -import sys - from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index d0735d1ec..b09a09762 100644 --- a/certbot-dns-luadns/setup.py +++ b/certbot-dns-luadns/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index aebff5304..e48428191 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-ovh/setup.py b/certbot-dns-ovh/setup.py index 68ede8006..8b6d73d33 100644 --- a/certbot-dns-ovh/setup.py +++ b/certbot-dns-ovh/setup.py @@ -1,10 +1,8 @@ -import sys - from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index 914bfb6e6..edf7b6ba6 100644 --- a/certbot-dns-rfc2136/setup.py +++ b/certbot-dns-rfc2136/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index 6318cbb81..69c2c7ed3 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -1,7 +1,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-sakuracloud/setup.py b/certbot-dns-sakuracloud/setup.py index 5af4c8a00..05843d2ed 100644 --- a/certbot-dns-sakuracloud/setup.py +++ b/certbot-dns-sakuracloud/setup.py @@ -1,10 +1,8 @@ -import sys - from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index a5cf2892e..622eb8d55 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -415,7 +415,7 @@ def _parse_ssl_options(ssl_options): with open(ssl_options) as _file: return nginxparser.load(_file) except IOError: - logger.warn("Missing NGINX TLS options file: %s", ssl_options) + logger.warning("Missing NGINX TLS options file: %s", ssl_options) except pyparsing.ParseBaseException as err: logger.debug("Could not parse file: %s due to %s", ssl_options, err) return [] diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 2814cbb8c..957588e2a 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -194,9 +194,9 @@ class NginxConfiguratorTest(util.NginxTest): def test_ipv6only(self): # ipv6_info: (ipv6_active, ipv6only_present) - self.assertEquals((True, False), self.config.ipv6_info("80")) + self.assertEqual((True, False), self.config.ipv6_info("80")) # Port 443 has ipv6only=on because of ipv6ssl.com vhost - self.assertEquals((True, True), self.config.ipv6_info("443")) + self.assertEqual((True, True), self.config.ipv6_info("443")) def test_ipv6only_detection(self): self.config.version = (1, 3, 1) @@ -807,7 +807,7 @@ class NginxConfiguratorTest(util.NginxTest): self.assertTrue(vhost in mock_select_vhs.call_args[0][0]) # And the actual returned values - self.assertEquals(len(vhs), 1) + self.assertEqual(len(vhs), 1) self.assertEqual(vhs[0], vhost) def test_choose_vhosts_wildcard_redirect(self): @@ -823,7 +823,7 @@ class NginxConfiguratorTest(util.NginxTest): self.assertTrue(vhost in mock_select_vhs.call_args[0][0]) # And the actual returned values - self.assertEquals(len(vhs), 1) + self.assertEqual(len(vhs), 1) self.assertEqual(vhs[0], vhost) def test_deploy_cert_wildcard(self): @@ -838,7 +838,7 @@ class NginxConfiguratorTest(util.NginxTest): self.config.deploy_cert("*.com", "/tmp/path", "/tmp/path", "/tmp/path", "/tmp/path") self.assertTrue(mock_dep.called) - self.assertEquals(len(mock_dep.call_args_list), 1) + self.assertEqual(len(mock_dep.call_args_list), 1) self.assertEqual(vhost, mock_dep.call_args_list[0][0][0]) @mock.patch("certbot_nginx.display_ops.select_vhost_multiple") diff --git a/certbot-nginx/certbot_nginx/tests/parser_obj_test.py b/certbot-nginx/certbot_nginx/tests/parser_obj_test.py index c9c9dd440..2217be54f 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_obj_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_obj_test.py @@ -103,37 +103,37 @@ class SentenceTest(unittest.TestCase): def test_parse_sentence_words_hides_spaces(self): og_sentence = ['\r\n', 'hello', ' ', ' ', '\t\n ', 'lol', ' ', 'spaces'] self.sentence.parse(og_sentence) - self.assertEquals(self.sentence.words, ['hello', 'lol', 'spaces']) - self.assertEquals(self.sentence.dump(), ['hello', 'lol', 'spaces']) - self.assertEquals(self.sentence.dump(True), og_sentence) + self.assertEqual(self.sentence.words, ['hello', 'lol', 'spaces']) + self.assertEqual(self.sentence.dump(), ['hello', 'lol', 'spaces']) + self.assertEqual(self.sentence.dump(True), og_sentence) def test_parse_sentence_with_add_spaces(self): self.sentence.parse(['hi', 'there'], add_spaces=True) - self.assertEquals(self.sentence.dump(True), ['hi', ' ', 'there']) + self.assertEqual(self.sentence.dump(True), ['hi', ' ', 'there']) self.sentence.parse(['one', ' ', 'space', 'none'], add_spaces=True) - self.assertEquals(self.sentence.dump(True), ['one', ' ', 'space', ' ', 'none']) + self.assertEqual(self.sentence.dump(True), ['one', ' ', 'space', ' ', 'none']) def test_iterate(self): expected = [['1', '2', '3']] self.sentence.parse(['1', ' ', '2', ' ', '3']) for i, sentence in enumerate(self.sentence.iterate()): - self.assertEquals(sentence.dump(), expected[i]) + self.assertEqual(sentence.dump(), expected[i]) def test_set_tabs(self): self.sentence.parse(['tabs', 'pls'], add_spaces=True) self.sentence.set_tabs() - self.assertEquals(self.sentence.dump(True)[0], '\n ') + self.assertEqual(self.sentence.dump(True)[0], '\n ') self.sentence.parse(['tabs', 'pls'], add_spaces=True) def test_get_tabs(self): self.sentence.parse(['no', 'tabs']) - self.assertEquals(self.sentence.get_tabs(), '') + self.assertEqual(self.sentence.get_tabs(), '') self.sentence.parse(['\n \n ', 'tabs']) - self.assertEquals(self.sentence.get_tabs(), ' ') + self.assertEqual(self.sentence.get_tabs(), ' ') self.sentence.parse(['\n\t ', 'tabs']) - self.assertEquals(self.sentence.get_tabs(), '\t ') + self.assertEqual(self.sentence.get_tabs(), '\t ') self.sentence.parse(['\n\t \n', 'tabs']) - self.assertEquals(self.sentence.get_tabs(), '') + self.assertEqual(self.sentence.get_tabs(), '') class BlockTest(unittest.TestCase): def setUp(self): @@ -145,11 +145,11 @@ class BlockTest(unittest.TestCase): def test_iterate(self): # Iterates itself normally - self.assertEquals(self.bloc, next(self.bloc.iterate())) + self.assertEqual(self.bloc, next(self.bloc.iterate())) # Iterates contents while expanded expected = [self.bloc.dump()] + self.contents for i, elem in enumerate(self.bloc.iterate(expanded=True)): - self.assertEquals(expected[i], elem.dump()) + self.assertEqual(expected[i], elem.dump()) def test_iterate_match(self): # can match on contents while expanded @@ -157,17 +157,17 @@ class BlockTest(unittest.TestCase): expected = [['thing', '1'], ['thing', '2']] for i, elem in enumerate(self.bloc.iterate(expanded=True, match=lambda x: isinstance(x, Sentence) and 'thing' in x.words)): - self.assertEquals(expected[i], elem.dump()) + self.assertEqual(expected[i], elem.dump()) # can match on self - self.assertEquals(self.bloc, next(self.bloc.iterate( + self.assertEqual(self.bloc, next(self.bloc.iterate( expanded=True, match=lambda x: isinstance(x, Block) and 'server' in x.names))) def test_parse_with_added_spaces(self): import copy self.bloc.parse([copy.copy(self.name), self.contents], add_spaces=True) - self.assertEquals(self.bloc.dump(), [self.name, self.contents]) - self.assertEquals(self.bloc.dump(True), [ + self.assertEqual(self.bloc.dump(), [self.name, self.contents]) + self.assertEqual(self.bloc.dump(True), [ ['server', ' ', 'name', ' '], [['thing', ' ', '1'], ['thing', ' ', '2'], @@ -181,14 +181,14 @@ class BlockTest(unittest.TestCase): def test_set_tabs(self): self.bloc.set_tabs() - self.assertEquals(self.bloc.names.dump(True)[0], '\n ') + self.assertEqual(self.bloc.names.dump(True)[0], '\n ') for elem in self.bloc.contents.dump(True)[:-1]: - self.assertEquals(elem[0], '\n ') - self.assertEquals(self.bloc.contents.dump(True)[-1][0], '\n') + self.assertEqual(elem[0], '\n ') + self.assertEqual(self.bloc.contents.dump(True)[-1][0], '\n') def test_get_tabs(self): self.bloc.parse([[' \n \t', 'lol'], []]) - self.assertEquals(self.bloc.get_tabs(), ' \t') + self.assertEqual(self.bloc.get_tabs(), ' \t') class StatementsTest(unittest.TestCase): def setUp(self): @@ -210,7 +210,7 @@ class StatementsTest(unittest.TestCase): self.statements.parse(self.raw) self.statements.set_tabs() for statement in self.statements.iterate(): - self.assertEquals(statement.dump(True)[0], '\n ') + self.assertEqual(statement.dump(True)[0], '\n ') def test_set_tabs_with_parent(self): # Trailing whitespace should inherit from parent tabbing. @@ -219,19 +219,19 @@ class StatementsTest(unittest.TestCase): self.statements.parent.get_tabs.return_value = '\t\t' self.statements.set_tabs() for statement in self.statements.iterate(): - self.assertEquals(statement.dump(True)[0], '\n ') - self.assertEquals(self.statements.dump(True)[-1], '\n\t\t') + self.assertEqual(statement.dump(True)[0], '\n ') + self.assertEqual(self.statements.dump(True)[-1], '\n\t\t') def test_get_tabs(self): self.raw[0].insert(0, '\n \n \t') self.statements.parse(self.raw) - self.assertEquals(self.statements.get_tabs(), ' \t') + self.assertEqual(self.statements.get_tabs(), ' \t') self.statements.parse([]) - self.assertEquals(self.statements.get_tabs(), '') + self.assertEqual(self.statements.get_tabs(), '') def test_parse_with_added_spaces(self): self.statements.parse(self.raw, add_spaces=True) - self.assertEquals(self.statements.dump(True)[0], ['sentence', ' ', 'one']) + self.assertEqual(self.statements.dump(True)[0], ['sentence', ' ', 'one']) def test_parse_bad_list_raises_error(self): from certbot import errors @@ -241,13 +241,13 @@ class StatementsTest(unittest.TestCase): self.statements.parse(self.raw + ['\n\n ']) self.assertTrue(isinstance(self.statements.dump()[-1], list)) self.assertTrue(self.statements.dump(True)[-1].isspace()) - self.assertEquals(self.statements.dump(True)[-1], '\n\n ') + self.assertEqual(self.statements.dump(True)[-1], '\n\n ') def test_iterate(self): self.statements.parse(self.raw) expected = [['sentence', 'one'], ['sentence', 'two']] for i, elem in enumerate(self.statements.iterate(match=lambda x: 'sentence' in x)): - self.assertEquals(expected[i], elem.dump()) + self.assertEqual(expected[i], elem.dump()) if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 0908c4c52..70e11f62b 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.29.0.dev0' +version = '0.31.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-postfix/certbot_postfix/tests/postconf_test.py b/certbot-postfix/certbot_postfix/tests/postconf_test.py index 91617d410..01a43773d 100644 --- a/certbot-postfix/certbot_postfix/tests/postconf_test.py +++ b/certbot-postfix/certbot_postfix/tests/postconf_test.py @@ -85,7 +85,7 @@ class PostConfTest(unittest.TestCase): self.config.set('extra_param', 'another_value') self.config.flush() arguments = mock_out.call_args_list[-1][0][0] - self.assertEquals('-e', arguments[0]) + self.assertEqual('-e', arguments[0]) self.assertTrue('default_parameter=new_value' in arguments) self.assertTrue('extra_param=another_value' in arguments) diff --git a/certbot/__init__.py b/certbot/__init__.py index f6b7defbd..bf68034c8 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.29.0.dev0' +__version__ = '0.31.0.dev0' diff --git a/certbot/cli.py b/certbot/cli.py index 99bf33180..ff1827cb1 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -101,6 +101,7 @@ manage certificates: manage your account with Let's Encrypt: register Create a Let's Encrypt ACME account + update_account Update a Let's Encrypt ACME account --agree-tos Agree to the ACME server's Subscriber Agreement -m EMAIL Email address for important account notifications """ @@ -172,10 +173,11 @@ def possible_deprecation_warning(config): # need warnings return if "CERTBOT_AUTO" not in os.environ: - logger.warning("You are running with an old copy of letsencrypt-auto that does " - "not receive updates, and is less reliable than more recent versions. " - "We recommend upgrading to the latest certbot-auto script, or using native " - "OS packages.") + logger.warning("You are running with an old copy of letsencrypt-auto" + " that does not receive updates, and is less reliable than more" + " recent versions. The letsencrypt client has also been renamed" + " to Certbot. We recommend upgrading to the latest certbot-auto" + " script, or using native OS packages.") logger.debug("Deprecation warning circumstances: %s / %s", sys.argv[0], os.environ) @@ -286,7 +288,9 @@ def read_file(filename, mode="rb"): """ try: filename = os.path.abspath(filename) - return filename, open(filename, mode).read() + with open(filename, mode) as the_file: + contents = the_file.read() + return filename, contents except IOError as exc: raise argparse.ArgumentTypeError(exc.strerror) @@ -394,9 +398,14 @@ VERB_HELP = [ }), ("register", { "short": "Register for account with Let's Encrypt / other ACME server", - "opts": "Options for account registration & modification", + "opts": "Options for account registration", "usage": "\n\n certbot register --email user@example.com [options]\n\n" }), + ("update_account", { + "short": "Update existing account with Let's Encrypt / other ACME server", + "opts": "Options for account modification", + "usage": "\n\n certbot update_account --email updated_email@example.com [options]\n\n" + }), ("unregister", { "short": "Irrevocably deactivate your account", "opts": "Options for account deactivation.", @@ -462,6 +471,7 @@ class HelpfulArgumentParser(object): "install": main.install, "plugins": main.plugins_cmd, "register": main.register, + "update_account": main.update_account, "unregister": main.unregister, "renew": main.renew, "revoke": main.revoke, @@ -856,7 +866,9 @@ class HelpfulArgumentParser(object): if chosen_topic == "everything": chosen_topic = "run" if chosen_topic == "all": - return dict([(t, True) for t in self.help_topics]) + # Addition of condition closes #6209 (removal of duplicate route53 option). + return dict([(t, True) if t != 'certbot-route53:auth' else (t, False) + for t in self.help_topics]) elif not chosen_topic: return dict([(t, False) for t in self.help_topics]) else: @@ -941,6 +953,18 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "specified or you already have a certificate with the same " "name. In the case of a name collision it will append a number " "like 0001 to the file path name. (default: Ask)") + helpful.add( + [None, "run", "certonly", "register"], + "--eab-kid", dest="eab_kid", + metavar="EAB_KID", + help="Key Identifier for External Account Binding" + ) + helpful.add( + [None, "run", "certonly", "register"], + "--eab-hmac-key", dest="eab_hmac_key", + metavar="EAB_HMAC_KEY", + help="HMAC key for External Account Binding" + ) helpful.add( [None, "run", "certonly", "manage", "delete", "certificates", "renew", "enhance"], "--cert-name", dest="certname", @@ -976,21 +1000,21 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "certificates. Updates to the Subscriber Agreement will still " "affect you, and will be effective 14 days after posting an " "update to the web site.") + # TODO: When `certbot register --update-registration` is fully deprecated, + # delete following helpful.add helpful.add( "register", "--update-registration", action="store_true", - default=flag_default("update_registration"), - help="With the register verb, indicates that details associated " - "with an existing registration, such as the e-mail address, " - "should be updated, rather than registering a new account.") + default=flag_default("update_registration"), dest="update_registration", + help=argparse.SUPPRESS) helpful.add( - ["register", "unregister", "automation"], "-m", "--email", + ["register", "update_account", "unregister", "automation"], "-m", "--email", default=flag_default("email"), help=config_help("email")) - helpful.add(["register", "automation"], "--eff-email", action="store_true", + helpful.add(["register", "update_account", "automation"], "--eff-email", action="store_true", default=flag_default("eff_email"), dest="eff_email", help="Share your e-mail address with EFF") - helpful.add(["register", "automation"], "--no-eff-email", action="store_false", - default=flag_default("eff_email"), dest="eff_email", + helpful.add(["register", "update_account", "automation"], "--no-eff-email", + action="store_false", default=flag_default("eff_email"), dest="eff_email", help="Don't share your e-mail address with EFF") helpful.add( ["automation", "certonly", "run"], @@ -1191,6 +1215,10 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " one will be run.") helpful.add("renew", "--renew-hook", action=_RenewHookAction, help=argparse.SUPPRESS) + helpful.add( + "renew", "--no-random-sleep-on-renew", action="store_false", + default=flag_default("random_sleep_on_renew"), dest="random_sleep_on_renew", + help=argparse.SUPPRESS) helpful.add( "renew", "--deploy-hook", action=_DeployHookAction, help='Command to be run in a shell once for each successfully' diff --git a/certbot/client.py b/certbot/client.py index e634b6bd9..38b77a772 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -203,9 +203,27 @@ def perform_registration(acme, config, tos_cb): :returns: Registration Resource. :rtype: `acme.messages.RegistrationResource` """ + + eab_credentials_supplied = config.eab_kid and config.eab_hmac_key + if eab_credentials_supplied: + account_public_key = acme.client.net.key.public_key() + eab = messages.ExternalAccountBinding.from_data(account_public_key=account_public_key, + kid=config.eab_kid, + hmac_key=config.eab_hmac_key, + directory=acme.client.directory) + else: + eab = None + + if acme.external_account_required(): + if not eab_credentials_supplied: + msg = ("Server requires external account binding." + " Please use --eab-kid and --eab-hmac-key.") + raise errors.Error(msg) + try: - return acme.new_account_and_tos(messages.NewRegistration.from_data(email=config.email), - tos_cb) + newreg = messages.NewRegistration.from_data(email=config.email, + external_account_binding=eab) + return acme.new_account_and_tos(newreg, tos_cb) except messages.Error as e: if e.code == "invalidEmail" or e.code == "invalidContact": if config.noninteractive_mode: diff --git a/certbot/compat.py b/certbot/compat.py index d42febe81..7d936aa9d 100644 --- a/certbot/compat.py +++ b/certbot/compat.py @@ -172,3 +172,30 @@ def compare_file_modes(mode1, mode2): # Windows specific: most of mode bits are ignored on Windows. Only check user R/W rights. return (stat.S_IMODE(mode1) & stat.S_IREAD == stat.S_IMODE(mode2) & stat.S_IREAD and stat.S_IMODE(mode1) & stat.S_IWRITE == stat.S_IMODE(mode2) & stat.S_IWRITE) + +WINDOWS_DEFAULT_FOLDERS = { + 'config': 'C:\\Certbot', + 'work': 'C:\\Certbot\\lib', + 'logs': 'C:\\Certbot\\log', +} +LINUX_DEFAULT_FOLDERS = { + 'config': '/etc/letsencrypt', + 'work': '/var/lib/letsencrypt', + 'logs': '/var/log/letsencrypt', +} + +def get_default_folder(folder_type): + """ + Return the relevant default folder for the current OS + + :param str folder_type: The type of folder to retrieve (config, work or logs) + + :returns: The relevant default folder. + :rtype: str + + """ + if 'fcntl' in sys.modules: + # Linux specific + return LINUX_DEFAULT_FOLDERS[folder_type] + # Windows specific + return WINDOWS_DEFAULT_FOLDERS[folder_type] diff --git a/certbot/constants.py b/certbot/constants.py index a2de2d27a..7d0e0c879 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -4,7 +4,7 @@ import os import pkg_resources from acme import challenges - +from certbot import compat SETUPTOOLS_PLUGINS_ENTRY_POINT = "certbot.plugins" """Setuptools entry point group name for plugins.""" @@ -14,7 +14,7 @@ OLD_SETUPTOOLS_PLUGINS_ENTRY_POINT = "letsencrypt.plugins" CLI_DEFAULTS = dict( config_files=[ - "/etc/letsencrypt/cli.ini", + os.path.join(compat.get_default_folder('config'), 'cli.ini'), # http://freedesktop.org/wiki/Software/xdg-user-dirs/ os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"), "letsencrypt", "cli.ini"), @@ -68,6 +68,9 @@ CLI_DEFAULTS = dict( directory_hooks=True, reuse_key=False, disable_renew_updates=False, + random_sleep_on_renew=True, + eab_hmac_key=None, + eab_kid=None, # Subparsers num=None, @@ -85,9 +88,9 @@ CLI_DEFAULTS = dict( auth_cert_path="./cert.pem", auth_chain_path="./chain.pem", key_path=None, - config_dir="/etc/letsencrypt", - work_dir="/var/lib/letsencrypt", - logs_dir="/var/log/letsencrypt", + config_dir=compat.get_default_folder('config'), + work_dir=compat.get_default_folder('work'), + logs_dir=compat.get_default_folder('logs'), server="https://acme-v02.api.letsencrypt.org/directory", # Plugins parsers diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 942f8502f..c4a389cd5 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -458,7 +458,7 @@ def sha256sum(filename): :rtype: str """ sha256 = hashlib.sha256() - with open(filename, 'rU') as file_d: + with open(filename, 'r') as file_d: sha256.update(file_d.read().encode('UTF-8')) return sha256.hexdigest() diff --git a/certbot/display/ops.py b/certbot/display/ops.py index 1e15a8474..3dae1070b 100644 --- a/certbot/display/ops.py +++ b/certbot/display/ops.py @@ -4,9 +4,11 @@ import os import zope.component +from certbot import compat from certbot import errors from certbot import interfaces from certbot import util + from certbot.display import util as display_util logger = logging.getLogger(__name__) @@ -33,7 +35,8 @@ def get_email(invalid=False, optional=True): unsafe_suggestion = ("\n\nIf you really want to skip this, you can run " "the client with --register-unsafely-without-email " "but make sure you then backup your account key from " - "/etc/letsencrypt/accounts\n\n") + "{0}\n\n".format(os.path.join( + compat.get_default_folder('config'), 'accounts'))) if optional: if invalid: msg += unsafe_suggestion diff --git a/certbot/main.py b/certbot/main.py index 5d5251dd2..ac639bc80 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -652,7 +652,45 @@ def unregister(config, unused_plugins): def register(config, unused_plugins): - """Create or modify accounts on the server. + """Create accounts on the server. + + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: List of plugins (deprecated) + :type unused_plugins: `list` of `str` + + :returns: `None` or a string indicating and error + :rtype: None or str + + """ + # TODO: When `certbot register --update-registration` is fully deprecated, + # delete the true case of if block + if config.update_registration: + msg = ("Usage 'certbot register --update-registration' is deprecated.\n" + "Please use 'cerbot update_account [options]' instead.\n") + logger.warning(msg) + return update_account(config, unused_plugins) + + # Portion of _determine_account logic to see whether accounts already + # exist or not. + account_storage = account.AccountFileStorage(config) + accounts = account_storage.find_all() + + if len(accounts) > 0: + # TODO: add a flag to register a duplicate account (this will + # also require extending _determine_account's behavior + # or else extracting the registration code from there) + return ("There is an existing account; registration of a " + "duplicate account with this command is currently " + "unsupported.") + # _determine_account will register an account + _determine_account(config) + return + + +def update_account(config, unused_plugins): + """Modify accounts on the server. :param config: Configuration object :type config: interfaces.IConfig @@ -671,20 +709,6 @@ def register(config, unused_plugins): reporter_util = zope.component.getUtility(interfaces.IReporter) add_msg = lambda m: reporter_util.add_message(m, reporter_util.MEDIUM_PRIORITY) - # registering a new account - if not config.update_registration: - if len(accounts) > 0: - # TODO: add a flag to register a duplicate account (this will - # also require extending _determine_account's behavior - # or else extracting the registration code from there) - return ("There is an existing account; registration of a " - "duplicate account with this command is currently " - "unsupported.") - # _determine_account will register an account - _determine_account(config) - return - - # --update-registration if len(accounts) == 0: return "Could not find an existing account to update." if config.email is None: diff --git a/certbot/ocsp.py b/certbot/ocsp.py index d34110f88..049e14827 100644 --- a/certbot/ocsp.py +++ b/certbot/ocsp.py @@ -114,7 +114,7 @@ def _translate_ocsp_query(cert_path, ocsp_output, ocsp_errors): logger.info("OCSP revocation warning: %s", warning) return True else: - logger.warn("Unable to properly parse OCSP output: %s\nstderr:%s", + logger.warning("Unable to properly parse OCSP output: %s\nstderr:%s", ocsp_output, ocsp_errors) return False diff --git a/certbot/plugins/enhancements_test.py b/certbot/plugins/enhancements_test.py index b69dc9836..22f6f54e9 100644 --- a/certbot/plugins/enhancements_test.py +++ b/certbot/plugins/enhancements_test.py @@ -37,11 +37,11 @@ class EnhancementTest(test_util.ConfigTestCase): self.assertTrue([i for i in enabled if i["name"] == "somethingelse"]) def test_are_requested(self): - self.assertEquals( + self.assertEqual( len([i for i in enhancements.enabled_enhancements(self.config)]), 0) self.assertFalse(enhancements.are_requested(self.config)) self.config.auto_hsts = True - self.assertEquals( + self.assertEqual( len([i for i in enhancements.enabled_enhancements(self.config)]), 1) self.assertTrue(enhancements.are_requested(self.config)) @@ -57,7 +57,7 @@ class EnhancementTest(test_util.ConfigTestCase): lineage = "lineage" enhancements.enable(lineage, domains, self.mockinstaller, self.config) self.assertTrue(self.mockinstaller.enable_autohsts.called) - self.assertEquals(self.mockinstaller.enable_autohsts.call_args[0], + self.assertEqual(self.mockinstaller.enable_autohsts.call_args[0], (lineage, domains)) diff --git a/certbot/plugins/selection_test.py b/certbot/plugins/selection_test.py index 44d64ab8e..5f8e42516 100644 --- a/certbot/plugins/selection_test.py +++ b/certbot/plugins/selection_test.py @@ -197,7 +197,7 @@ class GetUnpreparedInstallerTest(test_util.ConfigTestCase): def test_no_installer_defined(self): self.config.configurator = None - self.assertEquals(self._call(), None) + self.assertEqual(self._call(), None) def test_no_available_installers(self): self.config.configurator = "apache" diff --git a/certbot/plugins/standalone_test.py b/certbot/plugins/standalone_test.py index 47f44ff77..9b741fc6f 100644 --- a/certbot/plugins/standalone_test.py +++ b/certbot/plugins/standalone_test.py @@ -72,6 +72,8 @@ class ServerManagerTest(unittest.TestCase): errors.StandaloneBindError, self.mgr.run, port, challenge_type=challenges.HTTP01) self.assertEqual(self.mgr.running(), {}) + some_server.close() + maybe_another_server.close() class SupportedChallengesActionTest(unittest.TestCase): diff --git a/certbot/renewal.py b/certbot/renewal.py index a1508fa60..4c592b27f 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -5,6 +5,9 @@ import itertools import logging import os import traceback +import sys +import time +import random import six import zope.component @@ -276,8 +279,10 @@ def _avoid_invalidating_lineage(config, lineage, original_server): "Do not renew a valid cert with one from a staging server!" # Some lineages may have begun with --staging, but then had production certs # added to them + with open(lineage.cert) as the_file: + contents = the_file.read() latest_cert = OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, open(lineage.cert).read()) + OpenSSL.crypto.FILETYPE_PEM, contents) # all our test certs are from happy hacker fake CA, though maybe one day # we should test more methodically now_valid = "fake" not in repr(latest_cert.get_issuer()).lower() @@ -370,7 +375,7 @@ def _renew_describe_results(config, renew_successes, renew_failures, disp.notification("\n".join(out), wrap=False) -def handle_renewal_request(config): +def handle_renewal_request(config): # pylint: disable=too-many-locals,too-many-branches,too-many-statements """Examine each lineage; renew if due and report results""" # This is trivially False if config.domains is empty @@ -394,6 +399,14 @@ def handle_renewal_request(config): renew_failures = [] renew_skipped = [] parse_failures = [] + + # Noninteractive renewals include a random delay in order to spread + # out the load on the certificate authority servers, even if many + # users all pick the same time for renewals. This delay precedes + # running any hooks, so that side effects of the hooks (such as + # shutting down a web service) aren't prolonged unnecessarily. + apply_random_sleep = not sys.stdin.isatty() and config.random_sleep_on_renew + for renewal_file in conf_files: disp = zope.component.getUtility(interfaces.IDisplay) disp.notification("Processing " + renewal_file, pause=False) @@ -422,6 +435,15 @@ def handle_renewal_request(config): from certbot import main plugins = plugins_disco.PluginsRegistry.find_all() if should_renew(lineage_config, renewal_candidate): + # Apply random sleep upon first renewal if needed + if apply_random_sleep: + sleep_time = random.randint(1, 60 * 8) + logger.info("Non-interactive renewal: random delay of %s seconds", + sleep_time) + time.sleep(sleep_time) + # We will sleep only once this day, folks. + apply_random_sleep = False + # domains have been restored into lineage_config by reconstitute # but they're unnecessary anyway because renew_cert here # will just grab them from the certificate diff --git a/certbot/storage.py b/certbot/storage.py index 4b8110072..7472df975 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -29,6 +29,7 @@ logger = logging.getLogger(__name__) ALL_FOUR = ("cert", "privkey", "chain", "fullchain") README = "README" CURRENT_VERSION = util.get_strict_version(certbot.__version__) +BASE_PRIVKEY_MODE = 0o600 def renewal_conf_files(config): @@ -40,7 +41,9 @@ def renewal_conf_files(config): :rtype: `list` of `str` """ - return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf")) + result = glob.glob(os.path.join(config.renewal_configs_dir, "*.conf")) + result.sort() + return result def renewal_file_for_certname(config, certname): """Return /path/to/certname.conf in the renewal conf directory""" @@ -792,7 +795,7 @@ class RenewableCert(object): May need to recover from rare interrupted / crashed states.""" if self.has_pending_deployment(): - logger.warn("Found a new cert /archive/ that was not linked to in /live/; " + logger.warning("Found a new cert /archive/ that was not linked to in /live/; " "fixing...") self.update_all_links_to(self.latest_common_version()) return False @@ -1035,9 +1038,11 @@ class RenewableCert(object): archive = full_archive_path(None, cli_config, lineagename) live_dir = _full_live_path(cli_config, lineagename) if os.path.exists(archive): + config_file.close() raise errors.CertStorageError( "archive directory exists for " + lineagename) if os.path.exists(live_dir): + config_file.close() raise errors.CertStorageError( "live directory exists for " + lineagename) os.mkdir(archive) @@ -1048,13 +1053,14 @@ class RenewableCert(object): # Put the data into the appropriate files on disk target = dict([(kind, os.path.join(live_dir, kind + ".pem")) for kind in ALL_FOUR]) + archive_target = dict([(kind, os.path.join(archive, kind + "1.pem")) + for kind in ALL_FOUR]) for kind in ALL_FOUR: - os.symlink(os.path.join(_relpath_from_file(archive, target[kind]), kind + "1.pem"), - target[kind]) + os.symlink(_relpath_from_file(archive_target[kind], target[kind]), target[kind]) with open(target["cert"], "wb") as f: logger.debug("Writing certificate to %s.", target["cert"]) f.write(cert) - with open(target["privkey"], "wb") as f: + with util.safe_open(archive_target["privkey"], "wb", chmod=BASE_PRIVKEY_MODE) as f: logger.debug("Writing private key to %s.", target["privkey"]) f.write(privkey) # XXX: Let's make sure to get the file permissions right here @@ -1118,14 +1124,15 @@ class RenewableCert(object): os.path.join(self.archive_dir, "{0}{1}.pem".format(kind, target_version))) for kind in ALL_FOUR]) + old_privkey = os.path.join( + self.archive_dir, "privkey{0}.pem".format(prior_version)) + # Distinguish the cases where the privkey has changed and where it # has not changed (in the latter case, making an appropriate symlink # to an earlier privkey version) if new_privkey is None: # The behavior below keeps the prior key by creating a new # symlink to the old key or the target of the old key symlink. - old_privkey = os.path.join( - self.archive_dir, "privkey{0}.pem".format(prior_version)) if os.path.islink(old_privkey): old_privkey = os.readlink(old_privkey) else: @@ -1133,9 +1140,16 @@ class RenewableCert(object): logger.debug("Writing symlink to old private key, %s.", old_privkey) os.symlink(old_privkey, target["privkey"]) else: - with open(target["privkey"], "wb") as f: + with util.safe_open(target["privkey"], "wb", chmod=BASE_PRIVKEY_MODE) as f: logger.debug("Writing new private key to %s.", target["privkey"]) f.write(new_privkey) + # Preserve gid and (mode & 074) from previous privkey in this lineage. + old_mode = stat.S_IMODE(os.stat(old_privkey).st_mode) & \ + (stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | \ + stat.S_IROTH) + mode = BASE_PRIVKEY_MODE | old_mode + os.chown(target["privkey"], -1, os.stat(old_privkey).st_gid) + os.chmod(target["privkey"], mode) # Save everything else with open(target["cert"], "wb") as f: diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index 1aef33b0c..84774ca77 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -39,9 +39,8 @@ class BaseCertManagerTest(test_util.ConfigTestCase): # We also create a file that isn't a renewal config in the same # location to test that logic that reads in all-and-only renewal # configs will ignore it and NOT attempt to parse it. - junk = open(os.path.join(self.config.renewal_configs_dir, "IGNORE.THIS"), "w") - junk.write("This file should be ignored!") - junk.close() + with open(os.path.join(self.config.renewal_configs_dir, "IGNORE.THIS"), "w") as junk: + junk.write("This file should be ignored!") def _set_up_config(self, domain, custom_archive): # TODO: maybe provide NamespaceConfig.make_dirs? @@ -589,7 +588,7 @@ class GetCertnameTest(unittest.TestCase): from certbot import cert_manager prompt = "Which certificate would you" self.mock_get_utility().menu.return_value = (display_util.OK, 0) - self.assertEquals( + self.assertEqual( cert_manager.get_certnames( self.config, "verb", allow_multiple=False), ['example.com']) self.assertTrue( @@ -603,11 +602,11 @@ class GetCertnameTest(unittest.TestCase): from certbot import cert_manager prompt = "custom prompt" self.mock_get_utility().menu.return_value = (display_util.OK, 0) - self.assertEquals( + self.assertEqual( cert_manager.get_certnames( self.config, "verb", allow_multiple=False, custom_prompt=prompt), ['example.com']) - self.assertEquals(self.mock_get_utility().menu.call_args[0][0], + self.assertEqual(self.mock_get_utility().menu.call_args[0][0], prompt) @mock.patch('certbot.storage.renewal_conf_files') @@ -631,7 +630,7 @@ class GetCertnameTest(unittest.TestCase): prompt = "Which certificate(s) would you" self.mock_get_utility().checklist.return_value = (display_util.OK, ['example.com']) - self.assertEquals( + self.assertEqual( cert_manager.get_certnames( self.config, "verb", allow_multiple=True), ['example.com']) self.assertTrue( @@ -646,11 +645,11 @@ class GetCertnameTest(unittest.TestCase): prompt = "custom prompt" self.mock_get_utility().checklist.return_value = (display_util.OK, ['example.com']) - self.assertEquals( + self.assertEqual( cert_manager.get_certnames( self.config, "verb", allow_multiple=True, custom_prompt=prompt), ['example.com']) - self.assertEquals( + self.assertEqual( self.mock_get_utility().checklist.call_args[0][0], prompt) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 69ef16597..e16a1bdcf 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -4,6 +4,7 @@ import unittest import os import tempfile import copy +import sys import mock import six @@ -41,6 +42,15 @@ class TestReadFile(TempDirTestCase): self.assertEqual(contents, test_contents) +class FlagDefaultTest(unittest.TestCase): + """Tests cli.flag_default""" + + def test_linux_directories(self): + if 'fcntl' in sys.modules: + self.assertEqual(cli.flag_default('config_dir'), '/etc/letsencrypt') + self.assertEqual(cli.flag_default('work_dir'), '/var/lib/letsencrypt') + self.assertEqual(cli.flag_default('logs_dir'), '/var/log/letsencrypt') + class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods '''Test the cli args entrypoint''' @@ -431,6 +441,11 @@ class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertRaises(errors.Error, self.parse, "--allow-subset-of-names -d *.example.org".split()) + def test_route53_no_revert(self): + for help_flag in ['-h', '--help']: + for topic in ['all', 'plugins', 'dns-route53']: + self.assertFalse('certbot-route53:auth' in self._help_output([help_flag, topic])) + class DefaultTest(unittest.TestCase): """Tests for certbot.cli._Default.""" diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 70ab4c798..330529fc6 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -13,6 +13,8 @@ from certbot import util import certbot.tests.util as test_util +from josepy import interfaces + KEY = test_util.load_vector("rsa512_key.pem") CSR_SAN = test_util.load_vector("csr-san_512.pem") @@ -64,9 +66,28 @@ class RegisterTest(test_util.ConfigTestCase): tos_cb = mock.MagicMock() return register(self.config, self.account_storage, tos_cb) + @staticmethod + def _public_key_mock(): + m = mock.Mock(__class__=interfaces.JSONDeSerializable) + m.to_partial_json.return_value = '{"a": 1}' + return m + + @staticmethod + def _new_acct_dir_mock(): + return "/acme/new-account" + + @staticmethod + def _true_mock(): + return True + + @staticmethod + def _false_mock(): + return False + def test_no_tos(self): with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: mock_client.new_account_and_tos().terms_of_service = "http://tos" + mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot.eff.handle_subscription") as mock_handle: with mock.patch("certbot.account.report_new_account"): mock_client().new_account_and_tos.side_effect = errors.Error @@ -78,7 +99,8 @@ class RegisterTest(test_util.ConfigTestCase): self.assertTrue(mock_handle.called) def test_it(self): - with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2"): + with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: + mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot.account.report_new_account"): with mock.patch("certbot.eff.handle_subscription"): self._call() @@ -91,6 +113,7 @@ class RegisterTest(test_util.ConfigTestCase): 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.BackwardsCompatibleClientV2") as mock_client: + mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot.eff.handle_subscription") as mock_handle: mock_client().new_account_and_tos.side_effect = [mx_err, mock.MagicMock()] self._call() @@ -104,6 +127,7 @@ class RegisterTest(test_util.ConfigTestCase): 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.BackwardsCompatibleClientV2") as mock_client: + mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot.eff.handle_subscription"): mock_client().new_account_and_tos.side_effect = [mx_err, mock.MagicMock()] self.assertRaises(errors.Error, self._call) @@ -115,7 +139,8 @@ class RegisterTest(test_util.ConfigTestCase): @mock.patch("certbot.client.logger") def test_without_email(self, mock_logger): with mock.patch("certbot.eff.handle_subscription") as mock_handle: - with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2"): + with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_clnt: + mock_clnt().external_account_required.side_effect = self._false_mock with mock.patch("certbot.account.report_new_account"): self.config.email = None self.config.register_unsafely_without_email = True @@ -129,6 +154,7 @@ class RegisterTest(test_util.ConfigTestCase): def test_dry_run_no_staging_account(self, _rep, mock_get_email): """Tests dry-run for no staging account, expect account created with no email""" with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: + mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot.eff.handle_subscription"): with mock.patch("certbot.account.report_new_account"): self.config.dry_run = True @@ -138,11 +164,53 @@ class RegisterTest(test_util.ConfigTestCase): # check Certbot created an account with no email. Contact should return empty self.assertFalse(mock_client().new_account_and_tos.call_args[0][0].contact) + def test_with_eab_arguments(self): + with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: + mock_client().client.directory.__getitem__ = mock.Mock( + side_effect=self._new_acct_dir_mock + ) + mock_client().external_account_required.side_effect = self._false_mock + with mock.patch("certbot.eff.handle_subscription"): + target = "certbot.client.messages.ExternalAccountBinding.from_data" + with mock.patch(target) as mock_eab_from_data: + self.config.eab_kid = "test-kid" + self.config.eab_hmac_key = "J2OAqW4MHXsrHVa_PVg0Y-L_R4SYw0_aL1le6mfblbE" + self._call() + + self.assertTrue(mock_eab_from_data.called) + + def test_without_eab_arguments(self): + with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: + mock_client().external_account_required.side_effect = self._false_mock + with mock.patch("certbot.eff.handle_subscription"): + target = "certbot.client.messages.ExternalAccountBinding.from_data" + with mock.patch(target) as mock_eab_from_data: + self.config.eab_kid = None + self.config.eab_hmac_key = None + self._call() + + self.assertFalse(mock_eab_from_data.called) + + def test_external_account_required_without_eab_arguments(self): + with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: + mock_client().client.net.key.public_key = mock.Mock(side_effect=self._public_key_mock) + mock_client().external_account_required.side_effect = self._true_mock + with mock.patch("certbot.eff.handle_subscription"): + with mock.patch("certbot.client.messages.ExternalAccountBinding.from_data"): + self.config.eab_kid = None + self.config.eab_hmac_key = None + + self.assertRaises(errors.Error, self._call) + 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.BackwardsCompatibleClientV2") as mock_client: + mock_client().client.directory.__getitem__ = mock.Mock( + side_effect=self._new_acct_dir_mock + ) + mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot.eff.handle_subscription") as mock_handle: mock_client().new_account_and_tos.side_effect = [mx_err, mock.MagicMock()] self.assertRaises(messages.Error, self._call) @@ -487,7 +555,7 @@ class EnhanceConfigTest(ClientTestCommon): self.config.hsts = True self._test_with_already_existing() self.assertTrue(mock_log.warning.called) - self.assertEquals(mock_log.warning.call_args[0][1], + self.assertEqual(mock_log.warning.call_args[0][1], 'Strict-Transport-Security') @mock.patch("certbot.client.logger") @@ -495,7 +563,7 @@ class EnhanceConfigTest(ClientTestCommon): self.config.redirect = True self._test_with_already_existing() self.assertTrue(mock_log.warning.called) - self.assertEquals(mock_log.warning.call_args[0][1], + self.assertEqual(mock_log.warning.call_args[0][1], 'redirect') def test_no_ask_hsts(self): diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 9de8c5e9a..9ad0ce87a 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -502,9 +502,9 @@ class ChooseValuesTest(unittest.TestCase): items = ["first", "second", "third"] mock_util().checklist.return_value = (display_util.OK, [items[2]]) result = self._call(items, None) - self.assertEquals(result, [items[2]]) + self.assertEqual(result, [items[2]]) self.assertTrue(mock_util().checklist.called) - self.assertEquals(mock_util().checklist.call_args[0][0], None) + self.assertEqual(mock_util().checklist.call_args[0][0], None) @test_util.patch_get_utility("certbot.display.ops.z_util") def test_choose_names_success_question(self, mock_util): @@ -512,9 +512,9 @@ class ChooseValuesTest(unittest.TestCase): question = "Which one?" mock_util().checklist.return_value = (display_util.OK, [items[1]]) result = self._call(items, question) - self.assertEquals(result, [items[1]]) + self.assertEqual(result, [items[1]]) self.assertTrue(mock_util().checklist.called) - self.assertEquals(mock_util().checklist.call_args[0][0], question) + self.assertEqual(mock_util().checklist.call_args[0][0], question) @test_util.patch_get_utility("certbot.display.ops.z_util") def test_choose_names_user_cancel(self, mock_util): @@ -522,9 +522,9 @@ class ChooseValuesTest(unittest.TestCase): question = "Want to cancel?" mock_util().checklist.return_value = (display_util.CANCEL, []) result = self._call(items, question) - self.assertEquals(result, []) + self.assertEqual(result, []) self.assertTrue(mock_util().checklist.called) - self.assertEquals(mock_util().checklist.call_args[0][0], question) + self.assertEqual(mock_util().checklist.call_args[0][0], question) if __name__ == "__main__": diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py index 5672a20bd..726eb0b0f 100644 --- a/certbot/tests/display/util_test.py +++ b/certbot/tests/display/util_test.py @@ -49,6 +49,7 @@ class InputWithTimeoutTest(unittest.TestCase): stdin.listen(1) with mock.patch("certbot.display.util.sys.stdin", stdin): self.assertRaises(errors.Error, self._call, timeout=0.001) + stdin.close() class FileOutputDisplayTest(unittest.TestCase): @@ -314,7 +315,11 @@ class FileOutputDisplayTest(unittest.TestCase): # Every IDisplay method implemented by FileDisplay must take # force_interactive to prevent workflow regressions. for name in interfaces.IDisplay.names(): # pylint: disable=no-member - arg_spec = inspect.getargspec(getattr(self.displayer, name)) + if six.PY2: + getargspec = inspect.getargspec # pylint: disable=no-member + else: + getargspec = inspect.getfullargspec # pylint: disable=no-member + arg_spec = getargspec(getattr(self.displayer, name)) self.assertTrue("force_interactive" in arg_spec.args) @@ -371,7 +376,12 @@ class NoninteractiveDisplayTest(unittest.TestCase): for name in interfaces.IDisplay.names(): # pylint: disable=no-member method = getattr(self.displayer, name) # asserts method accepts arbitrary keyword arguments - self.assertFalse(inspect.getargspec(method).keywords is None) + if six.PY2: + result = inspect.getargspec(method).keywords # pylint: disable=no-member + self.assertFalse(result is None) + else: + result = inspect.getfullargspec(method).varkw # pylint: disable=no-member + self.assertFalse(result is None) class SeparateListInputTest(unittest.TestCase): diff --git a/certbot/tests/log_test.py b/certbot/tests/log_test.py index 6588bf5ca..b82cc6ca1 100644 --- a/certbot/tests/log_test.py +++ b/certbot/tests/log_test.py @@ -86,6 +86,7 @@ class PostArgParseSetupTest(test_util.ConfigTestCase): self.memory_handler.close() self.stream_handler.close() self.temp_handler.close() + self.devnull.close() super(PostArgParseSetupTest, self).tearDown() def test_common(self): diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 8d6a3e7ae..786b91a94 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -520,6 +520,8 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met '--work-dir', self.config.work_dir, '--logs-dir', self.config.logs_dir, '--text'] + self.mock_sleep = mock.patch('time.sleep').start() + def tearDown(self): # Reset globals in cli reload_module(cli) @@ -944,8 +946,8 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met @mock.patch('certbot.crypto_util.notAfter') @test_util.patch_get_utility() def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter): - cert_path = '/etc/letsencrypt/live/foo.bar' - key_path = '/etc/letsencrypt/live/baz.qux' + cert_path = os.path.normpath(os.path.join(self.config.config_dir, 'live/foo.bar')) + key_path = os.path.normpath(os.path.join(self.config.config_dir, 'live/baz.qux')) date = '1970-01-01' mock_notAfter().date.return_value = date @@ -975,7 +977,8 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met reuse_key=False): # pylint: disable=too-many-locals,too-many-arguments,too-many-branches cert_path = test_util.vector_path('cert_512.pem') - chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem' + chain_path = os.path.normpath(os.path.join(self.config.config_dir, + 'live/foo.bar/fullchain.pem')) mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path, cert_path=cert_path, fullchain_path=chain_path) mock_lineage.should_autorenew.return_value = due_for_renewal @@ -1092,6 +1095,26 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met args = ["renew", "--reuse-key"] self._test_renewal_common(True, [], args=args, should_renew=True, reuse_key=True) + @mock.patch('sys.stdin') + def test_noninteractive_renewal_delay(self, stdin): + stdin.isatty.return_value = False + test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') + args = ["renew", "--dry-run", "-tvv"] + self._test_renewal_common(True, [], args=args, should_renew=True) + self.assertEqual(self.mock_sleep.call_count, 1) + # in main.py: + # sleep_time = random.randint(1, 60*8) + sleep_call_arg = self.mock_sleep.call_args[0][0] + self.assertTrue(1 <= sleep_call_arg <= 60*8) + + @mock.patch('sys.stdin') + def test_interactive_no_renewal_delay(self, stdin): + stdin.isatty.return_value = True + test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') + args = ["renew", "--dry-run", "-tvv"] + self._test_renewal_common(True, [], args=args, should_renew=True) + self.assertEqual(self.mock_sleep.call_count, 0) + @mock.patch('certbot.renewal.should_renew') def test_renew_skips_recent_certs(self, should_renew): should_renew.return_value = False @@ -1375,7 +1398,20 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met x = self._call_no_clientmock(["register", "--email", "user@example.org"]) self.assertTrue("There is an existing account" in x[0]) - def test_update_registration_no_existing_accounts(self): + def test_update_account_no_existing_accounts(self): + # with mock.patch('certbot.main.client') as mocked_client: + with mock.patch('certbot.main.account') as mocked_account: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = [] + x = self._call_no_clientmock( + ["update_account", "--email", + "user@example.org"]) + self.assertTrue("Could not find an existing account" in x[0]) + + # TODO: When `certbot register --update-registration` is fully deprecated, + # delete the following test + def test_update_registration_no_existing_accounts_deprecated(self): # with mock.patch('certbot.main.client') as mocked_client: with mock.patch('certbot.main.account') as mocked_account: mocked_storage = mock.MagicMock() @@ -1386,7 +1422,9 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met "user@example.org"]) self.assertTrue("Could not find an existing account" in x[0]) - def test_update_registration_unsafely(self): + # TODO: When `certbot register --update-registration` is fully deprecated, + # delete the following test + def test_update_registration_unsafely_deprecated(self): # This test will become obsolete when register --update-registration # supports removing an e-mail address from the account with mock.patch('certbot.main.account') as mocked_account: @@ -1400,7 +1438,39 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met @mock.patch('certbot.main.display_ops.get_email') @test_util.patch_get_utility() - def test_update_registration_with_email(self, mock_utility, mock_email): + def test_update_account_with_email(self, mock_utility, mock_email): + email = "user@example.com" + mock_email.return_value = email + 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 + mocked_storage.find_all.return_value = ["an account"] + mocked_det.return_value = (mock.MagicMock(), "foo") + cb_client = mock.MagicMock() + mocked_client.Client.return_value = cb_client + x = self._call_no_clientmock( + ["update_account"]) + # When registration change succeeds, the return value + # of register() is None + self.assertTrue(x[0] is None) + # and we got supposedly did update the registration from + # the server + self.assertTrue( + cb_client.acme.update_registration.called) + # and we saved the updated registration on disk + self.assertTrue(mocked_storage.save_regr.called) + self.assertTrue( + email in mock_utility().add_message.call_args[0][0]) + self.assertTrue(mock_handle.called) + + # TODO: When `certbot register --update-registration` is fully deprecated, + # delete the following test + @mock.patch('certbot.main.display_ops.get_email') + @test_util.patch_get_utility() + def test_update_registration_with_email_deprecated(self, mock_utility, mock_email): email = "user@example.com" mock_email.return_value = email with mock.patch('certbot.eff.handle_subscription') as mock_handle: @@ -1652,7 +1722,7 @@ class EnhanceTest(test_util.ConfigTestCase): mock_lineage.return_value = mock.MagicMock(chain_path="/tmp/nonexistent") self._call(['enhance', '--auto-hsts']) self.assertTrue(self.mockinstaller.enable_autohsts.called) - self.assertEquals(self.mockinstaller.enable_autohsts.call_args[0][1], + self.assertEqual(self.mockinstaller.enable_autohsts.call_args[0][1], ["example.com", "another.tld"]) @mock.patch('certbot.cert_manager.lineage_for_certname') diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py index 2d54274f0..55cd24adb 100644 --- a/certbot/tests/ocsp_test.py +++ b/certbot/tests/ocsp_test.py @@ -96,15 +96,15 @@ class OCSPTest(unittest.TestCase): self.assertEqual(ocsp._translate_ocsp_query(*openssl_happy), False) self.assertEqual(ocsp._translate_ocsp_query(*openssl_confused), False) self.assertEqual(mock_log.debug.call_count, 1) - self.assertEqual(mock_log.warn.call_count, 0) + self.assertEqual(mock_log.warning.call_count, 0) mock_log.debug.call_count = 0 self.assertEqual(ocsp._translate_ocsp_query(*openssl_unknown), False) self.assertEqual(mock_log.debug.call_count, 1) - self.assertEqual(mock_log.warn.call_count, 0) + self.assertEqual(mock_log.warning.call_count, 0) self.assertEqual(ocsp._translate_ocsp_query(*openssl_expired_ocsp), False) self.assertEqual(mock_log.debug.call_count, 2) self.assertEqual(ocsp._translate_ocsp_query(*openssl_broken), False) - self.assertEqual(mock_log.warn.call_count, 1) + self.assertEqual(mock_log.warning.call_count, 1) mock_log.info.call_count = 0 self.assertEqual(ocsp._translate_ocsp_query(*openssl_revoked), True) self.assertEqual(mock_log.info.call_count, 0) diff --git a/certbot/tests/renewupdater_test.py b/certbot/tests/renewupdater_test.py index 5a362072c..5fe188c42 100644 --- a/certbot/tests/renewupdater_test.py +++ b/certbot/tests/renewupdater_test.py @@ -53,7 +53,7 @@ class RenewUpdaterTest(test_util.ConfigTestCase): self.config.dry_run = True updater.run_generic_updaters(self.config, None, None) self.assertTrue(mock_log.called) - self.assertEquals(mock_log.call_args[0][0], + self.assertEqual(mock_log.call_args[0][0], "Skipping updaters in dry-run mode.") @mock.patch("certbot.updater.logger.debug") @@ -61,7 +61,7 @@ class RenewUpdaterTest(test_util.ConfigTestCase): self.config.dry_run = True updater.run_renewal_deployer(self.config, None, None) self.assertTrue(mock_log.called) - self.assertEquals(mock_log.call_args[0][0], + self.assertEqual(mock_log.call_args[0][0], "Skipping renewal deployer in dry-run mode.") @mock.patch('certbot.plugins.selection.get_unprepared_installer') diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 03d595652..d75f4f595 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -13,6 +13,7 @@ import six import certbot from certbot import cli +from certbot import compat from certbot import errors from certbot.storage import ALL_FOUR @@ -73,9 +74,8 @@ class BaseRenewableCertTest(test_util.ConfigTestCase): # We also create a file that isn't a renewal config in the same # location to test that logic that reads in all-and-only renewal # configs will ignore it and NOT attempt to parse it. - junk = open(os.path.join(self.config.config_dir, "renewal", "IGNORE.THIS"), "w") - junk.write("This file should be ignored!") - junk.close() + with open(os.path.join(self.config.config_dir, "renewal", "IGNORE.THIS"), "w") as junk: + junk.write("This file should be ignored!") self.defaults = configobj.ConfigObj() @@ -92,6 +92,8 @@ class BaseRenewableCertTest(test_util.ConfigTestCase): link) with open(link, "wb") as f: f.write(kind.encode('ascii') if value is None else value) + if kind == "privkey": + os.chmod(link, 0o600) def _write_out_ex_kinds(self): for kind in ALL_FOUR: @@ -264,12 +266,12 @@ class RenewableCertTests(BaseRenewableCertTest): mock_has_pending.return_value = False self.assertEqual(self.test_rc.ensure_deployed(), True) self.assertEqual(mock_update.call_count, 0) - self.assertEqual(mock_logger.warn.call_count, 0) + self.assertEqual(mock_logger.warning.call_count, 0) mock_has_pending.return_value = True self.assertEqual(self.test_rc.ensure_deployed(), False) self.assertEqual(mock_update.call_count, 1) - self.assertEqual(mock_logger.warn.call_count, 1) + self.assertEqual(mock_logger.warning.call_count, 1) def test_update_link_to(self): @@ -544,6 +546,47 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.exists(temp_config_file)) + @test_util.broken_on_windows + @mock.patch("certbot.storage.relevant_values") + def test_save_successor_maintains_group_mode(self, mock_rv): + # Mock relevant_values() to claim that all values are relevant here + # (to avoid instantiating parser) + mock_rv.side_effect = lambda x: x + for kind in ALL_FOUR: + self._write_out_kind(kind, 1) + self.test_rc.update_all_links_to(1) + self.assertTrue(compat.compare_file_modes( + os.stat(self.test_rc.version("privkey", 1)).st_mode, 0o600)) + os.chmod(self.test_rc.version("privkey", 1), 0o444) + # If no new key, permissions should be the same (we didn't write any keys) + self.test_rc.save_successor(1, b"newcert", None, b"new chain", self.config) + self.assertTrue(compat.compare_file_modes( + os.stat(self.test_rc.version("privkey", 2)).st_mode, 0o444)) + # If new key, permissions should be kept as 644 + self.test_rc.save_successor(2, b"newcert", b"new_privkey", b"new chain", self.config) + self.assertTrue(compat.compare_file_modes( + os.stat(self.test_rc.version("privkey", 3)).st_mode, 0o644)) + # If permissions reverted, next renewal will also revert permissions of new key + os.chmod(self.test_rc.version("privkey", 3), 0o400) + self.test_rc.save_successor(3, b"newcert", b"new_privkey", b"new chain", self.config) + self.assertTrue(compat.compare_file_modes( + os.stat(self.test_rc.version("privkey", 4)).st_mode, 0o600)) + + @test_util.broken_on_windows + @mock.patch("certbot.storage.relevant_values") + @mock.patch("certbot.storage.os.chown") + def test_save_successor_maintains_gid(self, mock_chown, mock_rv): + # Mock relevant_values() to claim that all values are relevant here + # (to avoid instantiating parser) + mock_rv.side_effect = lambda x: x + for kind in ALL_FOUR: + self._write_out_kind(kind, 1) + self.test_rc.update_all_links_to(1) + self.test_rc.save_successor(1, b"newcert", None, b"new chain", self.config) + self.assertFalse(mock_chown.called) + self.test_rc.save_successor(2, b"newcert", b"new_privkey", b"new chain", self.config) + self.assertTrue(mock_chown.called) + def _test_relevant_values_common(self, values): defaults = dict((option, cli.flag_default(option)) for option in ("authenticator", "installer", @@ -630,6 +673,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.config.live_dir, "README"))) self.assertTrue(os.path.exists(os.path.join( self.config.live_dir, "the-lineage.com", "README"))) + self.assertTrue(compat.compare_file_modes(os.stat(result.key_path).st_mode, 0o600)) with open(result.fullchain, "rb") as f: self.assertEqual(f.read(), b"cert" + b"chain") # Let's do it again and make sure it makes a different lineage diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index 45cc55249..6685b88c6 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -210,16 +210,21 @@ class UniqueFileTest(test_util.TempDirTestCase): fd, name = self._call() fd.write("bar") fd.close() - self.assertEqual(open(name).read(), "bar") + with open(name) as f: + self.assertEqual(f.read(), "bar") def test_right_mode(self): - self.assertTrue(compat.compare_file_modes(0o700, os.stat(self._call(0o700)[1]).st_mode)) - self.assertTrue(compat.compare_file_modes(0o600, os.stat(self._call(0o600)[1]).st_mode)) + fd1, name1 = self._call(0o700) + fd2, name2 = self._call(0o600) + self.assertTrue(compat.compare_file_modes(0o700, os.stat(name1).st_mode)) + self.assertTrue(compat.compare_file_modes(0o600, os.stat(name2).st_mode)) + fd1.close() + fd2.close() def test_default_exists(self): - name1 = self._call()[1] # create 0000_foo.txt - name2 = self._call()[1] - name3 = self._call()[1] + fd1, name1 = self._call() # create 0000_foo.txt + fd2, name2 = self._call() + fd3, name3 = self._call() self.assertNotEqual(name1, name2) self.assertNotEqual(name1, name3) @@ -236,6 +241,10 @@ class UniqueFileTest(test_util.TempDirTestCase): basename3 = os.path.basename(name3) self.assertTrue(basename3.endswith("foo.txt")) + fd1.close() + fd2.close() + fd3.close() + try: file_type = file @@ -255,13 +264,18 @@ class UniqueLineageNameTest(test_util.TempDirTestCase): f, path = self._call("wow") self.assertTrue(isinstance(f, file_type)) self.assertEqual(os.path.join(self.tempdir, "wow.conf"), path) + f.close() def test_multiple(self): + items = [] for _ in six.moves.range(10): - f, name = self._call("wow") + items.append(self._call("wow")) + f, name = items[-1] self.assertTrue(isinstance(f, file_type)) self.assertTrue(isinstance(name, six.string_types)) self.assertTrue("wow-0009.conf" in name) + for f, _ in items: + f.close() @mock.patch("certbot.util.os.fdopen") def test_failure(self, mock_fdopen): diff --git a/docs/challenges.rst b/docs/challenges.rst index 25d190147..ee8bb8e61 100644 --- a/docs/challenges.rst +++ b/docs/challenges.rst @@ -3,10 +3,9 @@ Challenges To receive a certificate from Let's Encrypt certificate authority (CA), you must pass a *challenge* to prove you control each of the domain names that will be listed in the certificate. A challenge is one of -three tasks that only someone who controls the domain should be able to accomplish: +a list of specified tasks that only someone who controls the domain should be able to accomplish, such as: * Posting a specified file in a specified location on a web site (the HTTP-01 challenge) -* Offering a specified temporary certificate on a web site (the TLS-SNI-01 challenge) * Posting a specified DNS record in the domain name system (the DNS-01 challenge) It’s possible to complete each type of challenge *automatically* (Certbot directly makes the necessary @@ -16,21 +15,21 @@ design favors performing challenges automatically, and this is the normal case f Some plugins offer an *authenticator*, meaning that they can satisfy challenges: -* Apache plugin: (TLS-SNI-01) Tries to edit your Apache configuration files to temporarily serve - a Certbot-generated certificate for a specified name. Use the Apache plugin when you're running - Certbot on a web server with Apache listening on port 443. -* NGINX plugin: (TLS-SNI-01) Tries to edit your NGINX configuration files to temporarily serve a - Certbot-generated certificate for a specified name. Use the NGINX plugin when you're running - Certbot on a web server with NGINX listening on port 443. +* Apache plugin: (HTTP-01) Tries to edit your Apache configuration files to temporarily serve files to + satisfy challenges from the certificate authority. Use the Apache plugin when you're running Certbot on a + web server with Apache listening on port 80. +* Nginx plugin: (HTTP-01) Tries to edit your nginx configuration files to temporarily serve files to + satisfy challenges from the certificate authority. Use the nginx plugin when you're running Certbot on a + web server with nginx listening on port 80. * Webroot plugin: (HTTP-01) Tries to place a file where it can be served over HTTP on port 80 by a web server running on your system. Use the Webroot plugin when you're running Certbot on a web server with any server application listening on port 80 serving files from a folder on disk in response. -* Standalone plugin: (TLS-SNI-01 or HTTP-01) Tries to run a temporary web server listening on either HTTP on - port 80 (for HTTP-01) or HTTPS on port 443 (for TLS-SNI-01). Use the Standalone plugin if no existing program - is listening to these ports. Choose TLS-SNI-01 or HTTP-01 using the `--preferred-challenges` option. +* Standalone plugin: (HTTP-01) Tries to run a temporary web server listening on HTTP on port 80. Use the + Standalone plugin if no existing program is listening to this port. * Manual plugin: (DNS-01 or HTTP-01) Either tells you what changes to make to your configuration or updates your DNS records using an external script (for DNS-01) or your webroot (for HTTP-01). Use the Manual - plugin if you have the technical knowledge to make configuration changes yourself when asked to do so. + plugin if you have the technical knowledge to make configuration changes yourself when asked to do so, + and are prepared to repeat these steps every time the certificate needs to be renewed. Tips for Challenges ------------------- @@ -63,20 +62,6 @@ HTTP-01 Challenge * When using the Standalone plugin, make sure another program is not already listening to port 80 on the server. * When using the Webroot plugin, make sure there is a web server listening on port 80. -TLS-SNI-01 Challenge -~~~~~~~~~~~~~~~~~~~~ - -* The TLS-SNI-01 challenge doesn’t work with content delivery networks (CDNs) - like CloudFlare and Akamai because the domain name is pointed at the CDN, not directly at your server. -* Make sure port 443 is open, publicly reachable from the Internet, and not blocked by a router or firewall. -* When using the Apache plugin, make sure you are running Apache and no other web server on port 443. -* When using the NGINX plugin, make sure you are running NGINX and no other web server on port 443. -* With either the Apache or NGINX plugin, certbot modifies your web server configuration. If you get - an error after successfully completing the challenge, then you have received a certificate but the - plugin was unable to modify your web server configuration, meaning that you'll have to install the certificate manually. - In that case, please file a bug to help us improve certbot! -* When using the Standalone plugin, make sure another program is not already listening to port 443 on the server. - DNS-01 Challenge ~~~~~~~~~~~~~~~~ diff --git a/docs/cli-help.txt b/docs/cli-help.txt index d26da361b..bad6275e7 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -29,6 +29,7 @@ manage certificates: manage your account with Let's Encrypt: register Create a Let's Encrypt ACME account + update_account Update a Let's Encrypt ACME account --agree-tos Agree to the ACME server's Subscriber Agreement -m EMAIL Email address for important account notifications @@ -67,6 +68,10 @@ optional arguments: with the same name. In the case of a name collision it will append a number like 0001 to the file path name. (default: Ask) + --eab-kid EAB_KID Key Identifier for External Account Binding (default: + None) + --eab-hmac-key EAB_HMAC_KEY + HMAC key for External Account Binding (default: None) --cert-name CERTNAME Certificate name to apply. This name is used by Certbot for housekeeping and in file paths; it doesn't affect the content of the certificate itself. To see @@ -108,7 +113,7 @@ optional arguments: case, and to know when to deprecate support for past Python versions and flags. If you wish to hide this information from the Let's Encrypt server, set this to - "". (default: CertbotACMEClient/0.28.0 + "". (default: CertbotACMEClient/0.30.0 (certbot(-auto); OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY (SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel). The flags encoded in the @@ -355,7 +360,7 @@ revoke: certificates. (default: None) register: - Options for account registration & modification + Options for account registration --register-unsafely-without-email Specifying this flag enables registering an account @@ -367,11 +372,6 @@ register: to the Subscriber Agreement will still affect you, and will be effective 14 days after posting an update to the web site. (default: False) - --update-registration - With the register verb, indicates that details - associated with an existing registration, such as the - e-mail address, should be updated, rather than - registering a new account. (default: False) -m EMAIL, --email EMAIL Email used for registration and recovery contact. Use comma to register multiple emails, ex: @@ -380,6 +380,9 @@ register: --no-eff-email Don't share your e-mail address with EFF (default: None) +update_account: + Options for account modification + unregister: Options for account deactivation. @@ -473,12 +476,13 @@ plugins: using Sakura Cloud for DNS). (default: False) apache: - Apache Web Server plugin - Beta + Apache Web Server plugin --apache-enmod APACHE_ENMOD - Path to the Apache 'a2enmod' binary (default: None) + Path to the Apache 'a2enmod' binary (default: a2enmod) --apache-dismod APACHE_DISMOD - Path to the Apache 'a2dismod' binary (default: None) + Path to the Apache 'a2dismod' binary (default: + a2dismod) --apache-le-vhost-ext APACHE_LE_VHOST_EXT SSL vhost configuration extension (default: -le- ssl.conf) @@ -492,25 +496,16 @@ apache: /var/log/apache2) --apache-challenge-location APACHE_CHALLENGE_LOCATION Directory path for challenge configuration (default: - /etc/apache2/other) + /etc/apache2) --apache-handle-modules APACHE_HANDLE_MODULES Let installer handle enabling required modules for you - (Only Ubuntu/Debian currently) (default: False) + (Only Ubuntu/Debian currently) (default: True) --apache-handle-sites APACHE_HANDLE_SITES Let installer handle enabling sites for you (Only - Ubuntu/Debian currently) (default: False) + Ubuntu/Debian currently) (default: True) --apache-ctl APACHE_CTL Full path to Apache control script (default: - apachectl) - -certbot-route53:auth: - Obtain certificates using a DNS TXT record (if you are using AWS Route53 - for DNS). - - --certbot-route53:auth-propagation-seconds CERTBOT_ROUTE53:AUTH_PROPAGATION_SECONDS - The number of seconds to wait for DNS to propagate - before asking the ACME server to verify the DNS - record. (default: 10) + apache2ctl) dns-cloudflare: Obtain certificates using a DNS TXT record (if you are using Cloudflare diff --git a/docs/conf.py b/docs/conf.py index 2e6c5a9b7..c72d1c1cf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,6 +17,8 @@ import os import re import sys +import sphinx + here = os.path.abspath(os.path.dirname(__file__)) @@ -33,14 +35,13 @@ sys.path.insert(0, os.path.abspath(os.path.join(here, '..'))) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.0' +needs_sphinx = '1.2' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', - 'sphinx.ext.imgconverter', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', @@ -48,6 +49,9 @@ extensions = [ 'repoze.sphinx.autointerface', ] +if sphinx.version_info >= (1, 6): + extensions.append('sphinx.ext.imgconverter') + autodoc_member_order = 'bysource' autodoc_default_flags = ['show-inheritance', 'private-members'] diff --git a/docs/contributing.rst b/docs/contributing.rst index ead4d7e2b..264db630f 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -186,8 +186,8 @@ Authenticators -------------- Authenticators are plugins that prove control of a domain name by solving a -challenge provided by the ACME server. ACME currently defines three types of -challenges: HTTP, TLS-SNI, and DNS, represented by classes in `acme.challenges`. +challenge provided by the ACME server. ACME currently defines several types of +challenges: HTTP, TLS-SNI (deprecated), TLS-ALPR, and DNS, represented by classes in `acme.challenges`. An authenticator plugin should implement support for at least one challenge type. An Authenticator indicates which challenges it supports by implementing @@ -215,7 +215,7 @@ support for IIS, Icecast and Plesk. Installers and Authenticators will oftentimes be the same class/object (because for instance both tasks can be performed by a webserver like nginx) though this is not always the case (the standalone plugin is an authenticator -that listens on port 443, but it cannot install certs; a postfix plugin would +that listens on port 80, but it cannot install certs; a postfix plugin would be an installer but not an authenticator). Installers and Authenticators are kept separate because @@ -359,7 +359,10 @@ Steps: 4. Run ``tox --skip-missing-interpreters`` to run the entire test suite including coverage. The ``--skip-missing-interpreters`` argument ignores missing versions of Python needed for running the tests. Fix any errors. -5. Submit the PR. +5. Submit the PR. Once your PR is open, please do not force push to the branch + containing your pull request to squash or amend commits. We use `squash + merges `_ on PRs and + rewriting commits makes changes harder to track between reviews. 6. Did your tests pass on Travis? If they didn't, fix any errors. Asking for help diff --git a/docs/install.rst b/docs/install.rst index fc6abad7a..35b262482 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -29,7 +29,7 @@ System Requirements Certbot currently requires Python 2.7 or 3.4+ running on a UNIX-like operating system. By default, it requires root access in order to write to ``/etc/letsencrypt``, ``/var/log/letsencrypt``, ``/var/lib/letsencrypt``; to -bind to ports 80 and 443 (if you use the ``standalone`` plugin) and to read and +bind to port 80 (if you use the ``standalone`` plugin) and to read and modify webserver configurations (if you use the ``apache`` or ``nginx`` plugins). If none of these apply to you, it is theoretically possible to run without root privileges, but for most users who want to avoid running an ACME diff --git a/docs/using.rst b/docs/using.rst index 1fa13e022..5e8675418 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -44,14 +44,13 @@ a combination_ of distinct authenticator and installer plugins. =========== ==== ==== =============================================================== ============================= Plugin Auth Inst Notes Challenge types (and port) =========== ==== ==== =============================================================== ============================= -apache_ Y Y | Automates obtaining and installing a certificate with Apache tls-sni-01_ (443) +apache_ Y Y | Automates obtaining and installing a certificate with Apache http-01_ (80) | 2.4 on OSes with ``libaugeas0`` 1.0+. +nginx_ Y Y | Automates obtaining and installing a certificate with Nginx. http-01_ (80) webroot_ Y N | Obtains a certificate by writing to the webroot directory of http-01_ (80) | an already running webserver. -nginx_ Y Y | Automates obtaining and installing a certificate with Nginx. tls-sni-01_ (443) - | Shipped with Certbot 0.9.0. -standalone_ Y N | Uses a "standalone" webserver to obtain a certificate. http-01_ (80) or - | Requires port 80 or 443 to be available. This is useful on tls-sni-01_ (443) +standalone_ Y N | Uses a "standalone" webserver to obtain a certificate. http-01_ (80) + | Requires port 80 to be available. This is useful on | systems with no webserver, or when direct integration with | the local webserver is not supported or not desired. |dns_plugs| Y N | This category of plugins automates obtaining a certificate by dns-01_ (53) @@ -59,17 +58,17 @@ standalone_ Y N | Uses a "standalone" webserver to obtain a certificate. | domain. Doing domain validation in this way is | the only way to obtain wildcard certificates from Let's | Encrypt. -manual_ Y N | Helps you obtain a certificate by giving you instructions to http-01_ (80), - | perform domain validation yourself. Additionally allows you dns-01_ (53) or - | to specify scripts to automate the validation task in a tls-sni-01_ (443) +manual_ Y N | Helps you obtain a certificate by giving you instructions to http-01_ (80) or + | perform domain validation yourself. Additionally allows you dns-01_ (53) + | to specify scripts to automate the validation task in a | customized way. =========== ==== ==== =============================================================== ============================= .. |dns_plugs| replace:: :ref:`DNS plugins ` 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_ (requiring configuration of a DNS server on +prove you control a domain. The options are http-01_ (which uses port 80) +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``. @@ -78,7 +77,6 @@ There are also many third-party-plugins_ available. Below we describe in more de the circumstances in which each plugin can be used, and how to use it. .. _challenges: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7 -.. _tls-sni-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.3 .. _http-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.2 .. _dns-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.4 @@ -159,13 +157,9 @@ software running on the machine where you obtain the certificate. To obtain a certificate using a "standalone" webserver, you can use the standalone plugin by including ``certonly`` and ``--standalone`` -on the command line. This plugin needs to bind to port 80 or 443 in +on the command line. This plugin needs to bind to port 80 in order to perform domain validation, so you may need to stop your -existing webserver. To control which port the plugin uses, include -one of the options shown below on the command line. - - * ``--preferred-challenges http`` to use port 80 - * ``--preferred-challenges tls-sni`` to use port 443 +existing webserver. It must still be possible for your machine to accept inbound connections from the Internet on the specified port using each requested domain name. @@ -222,8 +216,7 @@ the UI, you can use the plugin to obtain a certificate by specifying to copy and paste commands into another terminal session, which may be on a different computer. -The manual plugin can use either the ``http``, ``dns`` or the -``tls-sni`` challenge. You can use the ``--preferred-challenges`` option +The manual plugin can use either the ``http`` or the ``dns`` challenge. You can use the ``--preferred-challenges`` option to choose the challenge of your preference. The ``http`` challenge will ask you to place a file with a specific name and @@ -241,11 +234,6 @@ For example, for the domain ``example.com``, a zone file entry would look like: _acme-challenge.example.com. 300 IN TXT "gfj9Xq...Rg85nM" -When using the ``tls-sni`` challenge, ``certbot`` will prepare a self-signed -SSL certificate for you with the challenge validation appropriately -encoded into a subjectAlternatNames entry. You will need to configure -your SSL server to present this challenge SSL certificate to the ACME -server using SNI. Additionally you can specify scripts to prepare for validation and perform the authentication procedure and/or clean up after it by using @@ -262,16 +250,20 @@ installer plugins. To do so, specify the authenticator plugin with ``--authenticator`` or ``-a`` and the installer plugin with ``--installer`` or ``-i``. -For instance, you may want to create a certificate using the webroot_ plugin -for authentication and the apache_ plugin for installation, perhaps because you -use a proxy or CDN for SSL and only want to secure the connection between them -and your origin server, which cannot use the tls-sni-01_ challenge due to the -intermediate proxy. +For instance, you could create a certificate using the webroot_ plugin +for authentication and the apache_ plugin for installation. :: certbot run -a webroot -i apache -w /var/www/html -d example.com +Or you could create a certificate using the manual_ plugin for authentication +and the nginx_ plugin for installation. (Note that this certificate cannot +be renewed automatically.) + +:: + certbot run -a manual -i nginx -d example.com + .. _third-party-plugins: Third-party plugins @@ -696,7 +688,9 @@ Where are my certificates? ========================== All generated keys and issued certificates can be found in -``/etc/letsencrypt/live/$domain``. Rather than copying, please point +``/etc/letsencrypt/live/$domain``. In the case of creating a SAN certificate +with multiple alternative names, ``$domain`` is the first domain passed in +via -d parameter. Rather than copying, please point your (web) server configuration directly to those files (or create symlinks). During the renewal_, ``/etc/letsencrypt/live`` is updated with the latest necessary files. @@ -715,6 +709,10 @@ The following files are available: put it into a safe, however - your server still needs to access this file in order for SSL/TLS to work. + .. note:: As of Certbot version 0.29.0, private keys for new certificate + default to ``0600``. Any changes to the group mode or group owner (gid) + of this file will be preserved on renewals. + This is what Apache needs for `SSLCertificateKeyFile `_, and Nginx for `ssl_certificate_key @@ -775,9 +773,6 @@ variables to these scripts: - ``CERTBOT_DOMAIN``: The domain being authenticated - ``CERTBOT_VALIDATION``: The validation string (HTTP-01 and DNS-01 only) - ``CERTBOT_TOKEN``: Resource name part of the HTTP-01 challenge (HTTP-01 only) -- ``CERTBOT_CERT_PATH``: The challenge SSL certificate (TLS-SNI-01 only) -- ``CERTBOT_KEY_PATH``: The private key associated with the aforementioned SSL certificate (TLS-SNI-01 only) -- ``CERTBOT_SNI_DOMAIN``: The SNI name for which the ACME server expects to be presented the self-signed certificate located at ``$CERTBOT_CERT_PATH`` (TLS-SNI-01 only) Additionally for cleanup: diff --git a/letsencrypt-auto b/letsencrypt-auto index fe87317a7..be2c3679b 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then fi VENV_BIN="$VENV_PATH/bin" BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt" -LE_AUTO_VERSION="0.28.0" +LE_AUTO_VERSION="0.30.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -593,8 +593,7 @@ BootstrapArchCommon() { # - ArchLinux (x86_64) # # "python-virtualenv" is Python3, but "python2-virtualenv" provides - # only "virtualenv2" binary, not "virtualenv" necessary in - # ./tools/_venv_common.sh + # only "virtualenv2" binary, not "virtualenv". deps=" python2 @@ -912,6 +911,35 @@ OldVenvExists() { [ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ] } +# Given python path, version 1 and version 2, check if version 1 is outdated compared to version 2. +# An unofficial version provided as version 1 (eg. 0.28.0.dev0) will be treated +# specifically by printing "UNOFFICIAL". Otherwise, print "OUTDATED" if version 1 +# is outdated, and "UP_TO_DATE" if not. +# This function relies only on installed python environment (2.x or 3.x) by certbot-auto. +CompareVersions() { + "$1" - "$2" "$3" << "UNLIKELY_EOF" +import sys +from distutils.version import StrictVersion + +try: + current = StrictVersion(sys.argv[1]) +except ValueError: + sys.stdout.write('UNOFFICIAL') + sys.exit() + +try: + remote = StrictVersion(sys.argv[2]) +except ValueError: + sys.stdout.write('UP_TO_DATE') + sys.exit() + +if current < remote: + sys.stdout.write('OUTDATED') +else: + sys.stdout.write('UP_TO_DATE') +UNLIKELY_EOF +} + if [ "$1" = "--le-auto-phase2" ]; then # Phase 2: Create venv, install LE, and run. @@ -1017,43 +1045,39 @@ pycparser==2.14 \ asn1crypto==0.22.0 \ --hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \ --hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a -cffi==1.10.0 \ - --hash=sha256:446699c10f3c390633d0722bc19edbc7ac4b94761918a4a4f7908a24e86ebbd0 \ - --hash=sha256:562326fc7f55a59ef3fef5e82908fe938cdc4bbda32d734c424c7cd9ed73e93a \ - --hash=sha256:7f732ad4a30db0b39400c3f7011249f7d0701007d511bf09604729aea222871f \ - --hash=sha256:94fb8410c6c4fc48e7ea759d3d1d9ca561171a88d00faddd4aa0306f698ad6a0 \ - --hash=sha256:587a5043df4b00a2130e09fed42da02a4ed3c688bd9bf07a3ac89d2271f4fb07 \ - --hash=sha256:ec08b88bef627ec1cea210e1608c85d3cf44893bcde74e41b7f7dbdfd2c1bad6 \ - --hash=sha256:a41406f6d62abcdf3eef9fd998d8dcff04fd2a7746644143045feeebd76352d1 \ - --hash=sha256:b560916546b2f209d74b82bdbc3223cee9a165b0242fa00a06dfc48a2054864a \ - --hash=sha256:e74896774e437f4715c57edeb5cf3d3a40d7727f541c2c12156617b5a15d1829 \ - --hash=sha256:9a31c18ba4881a116e448c52f3f5d3e14401cf7a9c43cc88f06f2a7f5428da0e \ - --hash=sha256:80796ea68e11624a0279d3b802f88a7fe7214122b97a15a6c97189934a2cc776 \ - --hash=sha256:f4019826a2dec066c909a1f483ef0dcf9325d6740cc0bd15308942b28b0930f7 \ - --hash=sha256:7248506981eeba23888b4140a69a53c4c0c0a386abcdca61ed8dd790a73e64b9 \ - --hash=sha256:a8955265d146e86fe2ce116394be4eaf0cb40314a79b19f11c4fa574cd639572 \ - --hash=sha256:c49187260043bd4c1d6a52186f9774f17d9b1da0a406798ebf4bfc12da166ade \ - --hash=sha256:c1d8b3d8dcb5c23ac1a8bf56422036f3f305a3c5a8bc8c354256579a1e2aa2c1 \ - --hash=sha256:9e389615bcecb8c782a87939d752340bb0a3a097e90bae54d7f0915bc12f45bd \ - --hash=sha256:d09ff358f75a874f69fa7d1c2b4acecf4282a950293fcfcf89aa606da8a9a500 \ - --hash=sha256:b69b4557aae7de18b7c174a917fe19873529d927ac592762d9771661875bbd40 \ - --hash=sha256:5de52b081a2775e76b971de9d997d85c4457fc0a09079e12d66849548ae60981 \ - --hash=sha256:e7d88fecb7b6250a1fd432e6dc64890342c372fce13dbfe4bb6f16348ad00c14 \ - --hash=sha256:1426e67e855ef7f5030c9184f4f1a9f4bfa020c31c962cd41fd129ec5aef4a6a \ - --hash=sha256:267dd2c66a5760c5f4d47e2ebcf8eeac7ef01e1ae6ae7a6d0d241a290068bc38 \ - --hash=sha256:e553eb489511cacf19eda6e52bc9e151316f0d721724997dda2c4d3079b778db \ - --hash=sha256:98b89b2c57f97ce2db7aeba60db173c84871d73b40e41a11ea95de1500ddc57e \ - --hash=sha256:e2b7e090188833bc58b2ae03fb864c22688654ebd2096bcf38bc860c4f38a3d8 \ - --hash=sha256:afa7d8b8d38ad40db8713ee053d41b36d87d6ae5ec5ad36f9210b548a18dc214 \ - --hash=sha256:4fc9c2ff7924b3a1fa326e1799e5dd58cac585d7fb25fe53ccaa1333b0453d65 \ - --hash=sha256:937db39a1ec5af3003b16357b2042bba67c88d43bc11aaa203fa8a5924524209 \ - --hash=sha256:ab22285797631df3b513b2cd3ecdc51cd8e3d36788e3991d93d0759d6883b027 \ - --hash=sha256:96e599b924ef009aa867f725b3249ee51d76489f484d3a45b4bd219c5ec6ed59 \ - --hash=sha256:bea842a0512be6a8007e585790bccd5d530520fc025ce63b03e139be373b0063 \ - --hash=sha256:e7175287f7fe7b1cc203bb958b17db40abd732690c1e18e700f10e0843a58598 \ - --hash=sha256:285ab352552f52f1398c912556d4d36d4ea9b8450e5c65d03809bf9886755533 \ - --hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \ - --hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5 +cffi==1.11.5 \ + --hash=sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50 \ + --hash=sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596 \ + --hash=sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef \ + --hash=sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743 \ + --hash=sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f \ + --hash=sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31 \ + --hash=sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04 \ + --hash=sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6 \ + --hash=sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3 \ + --hash=sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6 \ + --hash=sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b \ + --hash=sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca \ + --hash=sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e \ + --hash=sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb \ + --hash=sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd \ + --hash=sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1 \ + --hash=sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917 \ + --hash=sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359 \ + --hash=sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f \ + --hash=sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95 \ + --hash=sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801 \ + --hash=sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257 \ + --hash=sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184 \ + --hash=sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc \ + --hash=sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085 \ + --hash=sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93 \ + --hash=sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2 \ + --hash=sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30 \ + --hash=sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5 \ + --hash=sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e \ + --hash=sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b \ + --hash=sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4 ConfigArgParse==0.12.0 \ --hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339 \ --no-binary ConfigArgParse @@ -1146,9 +1170,9 @@ pytz==2015.7 \ --hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \ --hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \ --hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3 -requests==2.12.1 \ - --hash=sha256:3f3f27a9d0f9092935efc78054ef324eb9f8166718270aefe036dfa1e4f68e1e \ - --hash=sha256:2109ecea94df90980be040490ff1d879971b024861539abb00054062388b612e +requests==2.20.0 \ + --hash=sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c \ + --hash=sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279 six==1.10.0 \ --hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \ --hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a @@ -1185,6 +1209,15 @@ zope.interface==4.1.3 \ requests-toolbelt==0.8.0 \ --hash=sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237 \ --hash=sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5 +chardet==3.0.2 \ + --hash=sha256:4f7832e7c583348a9eddd927ee8514b3bf717c061f57b21dbe7697211454d9bb \ + --hash=sha256:6ebf56457934fdce01fb5ada5582762a84eed94cad43ed877964aebbdd8174c0 +urllib3==1.24.1 \ + --hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \ + --hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22 +certifi==2017.4.17 \ + --hash=sha256:f4318671072f030a33c7ca6acaef720ddd50ff124d1388e50c1bda4cbd6d7010 \ + --hash=sha256:f7527ebf7461582ce95f7a9e03dd141ce810d40590834f4ec20cddd54234c10a # Contains the requirements for the letsencrypt package. # @@ -1197,31 +1230,29 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.28.0 \ - --hash=sha256:f2f7c816acd695cbcda713a779b0db2b08e9de407146b46e1c4ef5561e0f5417 \ - --hash=sha256:31e3e2ee2a25c009a621c59ac9182f85d937a897c7bd1d47d0e01f3c712a090a -acme==0.28.0 \ - --hash=sha256:d3a564031155fece3f6edce8c5246d4cf34be0977b4c7c5ce84e86c68601c895 \ - --hash=sha256:bf7c2f1c24a26ab5b9fce3a6abca1d74a5914d46919649ae00ad5817db62bb85 -certbot-apache==0.28.0 \ - --hash=sha256:a57d7bac4f13ae5ecea4f4cbd479d381c02316c4832b25ab5a9d7c6826166370 \ - --hash=sha256:3f93f5de4a548e973c493a6cac5eeeb3dbbcae2988b61299ea0727d04a00f5bb -certbot-nginx==0.28.0 \ - --hash=sha256:1822a65910f0801087fa20a3af3fc94f878f93e0f11809483bb5387df861e296 \ - --hash=sha256:426fb403b0a7b203629f4e350a862cbc3bc1f69936fdab8ec7eafe0d8a3b5ddb +certbot==0.30.0 \ + --hash=sha256:b3468e128e74d2295598f6d3fbf9d0edfb67fe5abaca3b985a9e858395bd027f \ + --hash=sha256:d631fe6c75700ce9b2fdae194ff8b53c7518545d87dd451a1704f7572dcd49e8 +acme==0.30.0 \ + --hash=sha256:eed9389f802ebf4988c9e43c28ad3d5c2734237371d78e97450a1d61189a15aa \ + --hash=sha256:984b6d00bec73dcfa616636a760e80ca14bd246fb908710a656547f542f09445 +certbot-apache==0.30.0 \ + --hash=sha256:d38c70fc6930db298ea992a3145362eebdce460d3d2651f86a8f2f43d838c6d0 \ + --hash=sha256:1d4bc207d53a3e5d37e5d9ebd05f26089aa21d1fbf384113ed9d1829b4d1e9bf +certbot-nginx==0.30.0 \ + --hash=sha256:6163c7d0080f59b4ebe510afcc6af2d2eebf15469275c3835866690db4d465d6 \ + --hash=sha256:e39a3f3d77cd4c653949cf066fb2211039fd2032665697c27b6e8501c7c2dd92 UNLIKELY_EOF # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/pipstrap.py" #!/usr/bin/env python """A small script that can act as a trust root for installing pip >=8 - Embed this in your project, and your VCS checkout is all you have to trust. In a post-peep era, this lets you claw your way to a hash-checking version of pip, with which you can install the rest of your dependencies safely. All it assumes is Python 2.6 or better and *some* version of pip already installed. If anything goes wrong, it will exit with a non-zero status code. - """ # This is here so embedded copies are MIT-compliant: # Copyright (c) 2016 Erik Rose @@ -1348,10 +1379,8 @@ def hashed_download(url, temp, digest): def get_index_base(): """Return the URL to the dir containing the "packages" folder. - Try to wring something out of PIP_INDEX_URL, if set. Hack "/simple" off the end if it's there; that is likely to give us the right dir. - """ env_var = environ.get('PIP_INDEX_URL', '').rstrip('/') if env_var: @@ -1641,7 +1670,12 @@ UNLIKELY_EOF error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates." elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then error "WARNING: unable to check for updates." - elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then + fi + + LE_VERSION_STATE=`CompareVersions "$LE_PYTHON" "$LE_AUTO_VERSION" "$REMOTE_VERSION"` + if [ "$LE_VERSION_STATE" = "UNOFFICIAL" ]; then + say "Unofficial certbot-auto version detected, self-upgrade is disabled: $LE_AUTO_VERSION" + elif [ "$LE_VERSION_STATE" = "OUTDATED" ]; then say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." # Now we drop into Python so we don't have to install even more diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 57745758b..4e2a700e5 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- -iQEzBAABCAAdFiEEos+1H6J1pyhiNOeyTRfJlc2XdfIFAlvjV5wACgkQTRfJlc2X -dfKkRwf+MJ/Yo5ix7rxGMoliJl3GUUC2KvuYxObvbsAZW69Zl4aZVNeUP3Pe/EZj -zJlSMuiCPeTMmmr0+q78dk5Qk0vf+9D5qSQyy2U+RvPvX6z1PfaFXwjETwOEhE4i -7pABP4m/rIhlZbh336gou4XZK8sXsKHXBLQEyqmzPm6YFZ+5vowIoEinrN73PBuq -rgvoTFKi2NTjYNkQffYUeCIgO0pXlaOa8hkaupqoejHHEjjiXS2C9m0gAT2Wk2cO -zya5WQNcCCLWy/ChhPE2M7yRSpwqrszsHP0qo7QGL8vvsdXvNeJ7vwpAlq/9aipg -PpzSXy/ek8YAgApaj8+/w4OfdDhQ4Q== -=1hD2 +iQEzBAABCAAdFiEEos+1H6J1pyhiNOeyTRfJlc2XdfIFAlwtH9cACgkQTRfJlc2X +dfIqUwf/RXLZAeFF/59PjTAzcV+eEISlvEmFcV0zL3vv23PsY3S5Iuuwcd6rTm5M +UWNtmUTmFVo0xmxAj6Eqfpnt0P+JPpPcnbLNIGKFekBWIshgH84RRFWPJjNh/hu1 +pyzkkcWaOB86egdVfjvuRJ0j7AGd0ih6ur2rlgfHVjTYR+0EdWszFDEFBlq8cpct +9d1gCgH7VWKSIQMhzGLMsmdMxNoDl4hiqVPU0FP5/mn2xGF7FgeKNW3+NiTouKuB +mZOeEl3f3uOze/suHPyfOu+49jk+TWWE05Xfqfowjf486nKPg6/uSA2izW/MwIKN +HuIuY3bBf+lx5yUVIraoZhH2MxODDQ== +=BZqz -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 12be26e19..c08a08160 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then fi VENV_BIN="$VENV_PATH/bin" BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt" -LE_AUTO_VERSION="0.29.0.dev0" +LE_AUTO_VERSION="0.31.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -593,8 +593,7 @@ BootstrapArchCommon() { # - ArchLinux (x86_64) # # "python-virtualenv" is Python3, but "python2-virtualenv" provides - # only "virtualenv2" binary, not "virtualenv" necessary in - # ./tools/_venv_common.py + # only "virtualenv2" binary, not "virtualenv". deps=" python2 @@ -912,6 +911,35 @@ OldVenvExists() { [ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ] } +# Given python path, version 1 and version 2, check if version 1 is outdated compared to version 2. +# An unofficial version provided as version 1 (eg. 0.28.0.dev0) will be treated +# specifically by printing "UNOFFICIAL". Otherwise, print "OUTDATED" if version 1 +# is outdated, and "UP_TO_DATE" if not. +# This function relies only on installed python environment (2.x or 3.x) by certbot-auto. +CompareVersions() { + "$1" - "$2" "$3" << "UNLIKELY_EOF" +import sys +from distutils.version import StrictVersion + +try: + current = StrictVersion(sys.argv[1]) +except ValueError: + sys.stdout.write('UNOFFICIAL') + sys.exit() + +try: + remote = StrictVersion(sys.argv[2]) +except ValueError: + sys.stdout.write('UP_TO_DATE') + sys.exit() + +if current < remote: + sys.stdout.write('OUTDATED') +else: + sys.stdout.write('UP_TO_DATE') +UNLIKELY_EOF +} + if [ "$1" = "--le-auto-phase2" ]; then # Phase 2: Create venv, install LE, and run. @@ -1017,43 +1045,39 @@ pycparser==2.14 \ asn1crypto==0.22.0 \ --hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \ --hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a -cffi==1.10.0 \ - --hash=sha256:446699c10f3c390633d0722bc19edbc7ac4b94761918a4a4f7908a24e86ebbd0 \ - --hash=sha256:562326fc7f55a59ef3fef5e82908fe938cdc4bbda32d734c424c7cd9ed73e93a \ - --hash=sha256:7f732ad4a30db0b39400c3f7011249f7d0701007d511bf09604729aea222871f \ - --hash=sha256:94fb8410c6c4fc48e7ea759d3d1d9ca561171a88d00faddd4aa0306f698ad6a0 \ - --hash=sha256:587a5043df4b00a2130e09fed42da02a4ed3c688bd9bf07a3ac89d2271f4fb07 \ - --hash=sha256:ec08b88bef627ec1cea210e1608c85d3cf44893bcde74e41b7f7dbdfd2c1bad6 \ - --hash=sha256:a41406f6d62abcdf3eef9fd998d8dcff04fd2a7746644143045feeebd76352d1 \ - --hash=sha256:b560916546b2f209d74b82bdbc3223cee9a165b0242fa00a06dfc48a2054864a \ - --hash=sha256:e74896774e437f4715c57edeb5cf3d3a40d7727f541c2c12156617b5a15d1829 \ - --hash=sha256:9a31c18ba4881a116e448c52f3f5d3e14401cf7a9c43cc88f06f2a7f5428da0e \ - --hash=sha256:80796ea68e11624a0279d3b802f88a7fe7214122b97a15a6c97189934a2cc776 \ - --hash=sha256:f4019826a2dec066c909a1f483ef0dcf9325d6740cc0bd15308942b28b0930f7 \ - --hash=sha256:7248506981eeba23888b4140a69a53c4c0c0a386abcdca61ed8dd790a73e64b9 \ - --hash=sha256:a8955265d146e86fe2ce116394be4eaf0cb40314a79b19f11c4fa574cd639572 \ - --hash=sha256:c49187260043bd4c1d6a52186f9774f17d9b1da0a406798ebf4bfc12da166ade \ - --hash=sha256:c1d8b3d8dcb5c23ac1a8bf56422036f3f305a3c5a8bc8c354256579a1e2aa2c1 \ - --hash=sha256:9e389615bcecb8c782a87939d752340bb0a3a097e90bae54d7f0915bc12f45bd \ - --hash=sha256:d09ff358f75a874f69fa7d1c2b4acecf4282a950293fcfcf89aa606da8a9a500 \ - --hash=sha256:b69b4557aae7de18b7c174a917fe19873529d927ac592762d9771661875bbd40 \ - --hash=sha256:5de52b081a2775e76b971de9d997d85c4457fc0a09079e12d66849548ae60981 \ - --hash=sha256:e7d88fecb7b6250a1fd432e6dc64890342c372fce13dbfe4bb6f16348ad00c14 \ - --hash=sha256:1426e67e855ef7f5030c9184f4f1a9f4bfa020c31c962cd41fd129ec5aef4a6a \ - --hash=sha256:267dd2c66a5760c5f4d47e2ebcf8eeac7ef01e1ae6ae7a6d0d241a290068bc38 \ - --hash=sha256:e553eb489511cacf19eda6e52bc9e151316f0d721724997dda2c4d3079b778db \ - --hash=sha256:98b89b2c57f97ce2db7aeba60db173c84871d73b40e41a11ea95de1500ddc57e \ - --hash=sha256:e2b7e090188833bc58b2ae03fb864c22688654ebd2096bcf38bc860c4f38a3d8 \ - --hash=sha256:afa7d8b8d38ad40db8713ee053d41b36d87d6ae5ec5ad36f9210b548a18dc214 \ - --hash=sha256:4fc9c2ff7924b3a1fa326e1799e5dd58cac585d7fb25fe53ccaa1333b0453d65 \ - --hash=sha256:937db39a1ec5af3003b16357b2042bba67c88d43bc11aaa203fa8a5924524209 \ - --hash=sha256:ab22285797631df3b513b2cd3ecdc51cd8e3d36788e3991d93d0759d6883b027 \ - --hash=sha256:96e599b924ef009aa867f725b3249ee51d76489f484d3a45b4bd219c5ec6ed59 \ - --hash=sha256:bea842a0512be6a8007e585790bccd5d530520fc025ce63b03e139be373b0063 \ - --hash=sha256:e7175287f7fe7b1cc203bb958b17db40abd732690c1e18e700f10e0843a58598 \ - --hash=sha256:285ab352552f52f1398c912556d4d36d4ea9b8450e5c65d03809bf9886755533 \ - --hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \ - --hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5 +cffi==1.11.5 \ + --hash=sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50 \ + --hash=sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596 \ + --hash=sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef \ + --hash=sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743 \ + --hash=sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f \ + --hash=sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31 \ + --hash=sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04 \ + --hash=sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6 \ + --hash=sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3 \ + --hash=sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6 \ + --hash=sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b \ + --hash=sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca \ + --hash=sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e \ + --hash=sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb \ + --hash=sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd \ + --hash=sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1 \ + --hash=sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917 \ + --hash=sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359 \ + --hash=sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f \ + --hash=sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95 \ + --hash=sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801 \ + --hash=sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257 \ + --hash=sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184 \ + --hash=sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc \ + --hash=sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085 \ + --hash=sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93 \ + --hash=sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2 \ + --hash=sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30 \ + --hash=sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5 \ + --hash=sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e \ + --hash=sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b \ + --hash=sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4 ConfigArgParse==0.12.0 \ --hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339 \ --no-binary ConfigArgParse @@ -1146,9 +1170,9 @@ pytz==2015.7 \ --hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \ --hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \ --hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3 -requests==2.12.1 \ - --hash=sha256:3f3f27a9d0f9092935efc78054ef324eb9f8166718270aefe036dfa1e4f68e1e \ - --hash=sha256:2109ecea94df90980be040490ff1d879971b024861539abb00054062388b612e +requests==2.20.0 \ + --hash=sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c \ + --hash=sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279 six==1.10.0 \ --hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \ --hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a @@ -1185,6 +1209,15 @@ zope.interface==4.1.3 \ requests-toolbelt==0.8.0 \ --hash=sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237 \ --hash=sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5 +chardet==3.0.2 \ + --hash=sha256:4f7832e7c583348a9eddd927ee8514b3bf717c061f57b21dbe7697211454d9bb \ + --hash=sha256:6ebf56457934fdce01fb5ada5582762a84eed94cad43ed877964aebbdd8174c0 +urllib3==1.24.1 \ + --hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \ + --hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22 +certifi==2017.4.17 \ + --hash=sha256:f4318671072f030a33c7ca6acaef720ddd50ff124d1388e50c1bda4cbd6d7010 \ + --hash=sha256:f7527ebf7461582ce95f7a9e03dd141ce810d40590834f4ec20cddd54234c10a # Contains the requirements for the letsencrypt package. # @@ -1197,31 +1230,29 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.28.0 \ - --hash=sha256:f2f7c816acd695cbcda713a779b0db2b08e9de407146b46e1c4ef5561e0f5417 \ - --hash=sha256:31e3e2ee2a25c009a621c59ac9182f85d937a897c7bd1d47d0e01f3c712a090a -acme==0.28.0 \ - --hash=sha256:d3a564031155fece3f6edce8c5246d4cf34be0977b4c7c5ce84e86c68601c895 \ - --hash=sha256:bf7c2f1c24a26ab5b9fce3a6abca1d74a5914d46919649ae00ad5817db62bb85 -certbot-apache==0.28.0 \ - --hash=sha256:a57d7bac4f13ae5ecea4f4cbd479d381c02316c4832b25ab5a9d7c6826166370 \ - --hash=sha256:3f93f5de4a548e973c493a6cac5eeeb3dbbcae2988b61299ea0727d04a00f5bb -certbot-nginx==0.28.0 \ - --hash=sha256:1822a65910f0801087fa20a3af3fc94f878f93e0f11809483bb5387df861e296 \ - --hash=sha256:426fb403b0a7b203629f4e350a862cbc3bc1f69936fdab8ec7eafe0d8a3b5ddb +certbot==0.30.0 \ + --hash=sha256:b3468e128e74d2295598f6d3fbf9d0edfb67fe5abaca3b985a9e858395bd027f \ + --hash=sha256:d631fe6c75700ce9b2fdae194ff8b53c7518545d87dd451a1704f7572dcd49e8 +acme==0.30.0 \ + --hash=sha256:eed9389f802ebf4988c9e43c28ad3d5c2734237371d78e97450a1d61189a15aa \ + --hash=sha256:984b6d00bec73dcfa616636a760e80ca14bd246fb908710a656547f542f09445 +certbot-apache==0.30.0 \ + --hash=sha256:d38c70fc6930db298ea992a3145362eebdce460d3d2651f86a8f2f43d838c6d0 \ + --hash=sha256:1d4bc207d53a3e5d37e5d9ebd05f26089aa21d1fbf384113ed9d1829b4d1e9bf +certbot-nginx==0.30.0 \ + --hash=sha256:6163c7d0080f59b4ebe510afcc6af2d2eebf15469275c3835866690db4d465d6 \ + --hash=sha256:e39a3f3d77cd4c653949cf066fb2211039fd2032665697c27b6e8501c7c2dd92 UNLIKELY_EOF # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/pipstrap.py" #!/usr/bin/env python """A small script that can act as a trust root for installing pip >=8 - Embed this in your project, and your VCS checkout is all you have to trust. In a post-peep era, this lets you claw your way to a hash-checking version of pip, with which you can install the rest of your dependencies safely. All it assumes is Python 2.6 or better and *some* version of pip already installed. If anything goes wrong, it will exit with a non-zero status code. - """ # This is here so embedded copies are MIT-compliant: # Copyright (c) 2016 Erik Rose @@ -1260,7 +1291,7 @@ except ImportError: cmd = popenargs[0] raise CalledProcessError(retcode, cmd) return output -from sys import exit, version_info, executable +from sys import exit, version_info from tempfile import mkdtemp try: from urllib2 import build_opener, HTTPHandler, HTTPSHandler @@ -1272,7 +1303,7 @@ except ImportError: from urllib.parse import urlparse # 3.4 -__version__ = 2, 0, 0 +__version__ = 1, 5, 1 PIP_VERSION = '9.0.1' DEFAULT_INDEX_BASE = 'https://pypi.python.org' @@ -1348,10 +1379,8 @@ def hashed_download(url, temp, digest): def get_index_base(): """Return the URL to the dir containing the "packages" folder. - Try to wring something out of PIP_INDEX_URL, if set. Hack "/simple" off the end if it's there; that is likely to give us the right dir. - """ env_var = environ.get('PIP_INDEX_URL', '').rstrip('/') if env_var: @@ -1365,7 +1394,7 @@ def get_index_base(): def main(): - pip_version = StrictVersion(check_output([executable, '-m', 'pip', '--version']) + pip_version = StrictVersion(check_output(['pip', '--version']) .decode('utf-8').split()[1]) min_pip_version = StrictVersion(PIP_VERSION) if pip_version >= min_pip_version: @@ -1378,7 +1407,7 @@ def main(): temp, digest) for path, digest in PACKAGES] - check_output('{0} -m pip install --no-index --no-deps -U '.format(quote(executable)) + + check_output('pip install --no-index --no-deps -U ' + # Disable cache since we're not using it and it otherwise # sometimes throws permission warnings: ('--no-cache-dir ' if has_pip_cache else '') + @@ -1397,6 +1426,7 @@ def main(): if __name__ == '__main__': exit(main()) + UNLIKELY_EOF # ------------------------------------------------------------------------- # Set PATH so pipstrap upgrades the right (v)env: @@ -1640,7 +1670,12 @@ UNLIKELY_EOF error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates." elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then error "WARNING: unable to check for updates." - elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then + fi + + LE_VERSION_STATE=`CompareVersions "$LE_PYTHON" "$LE_AUTO_VERSION" "$REMOTE_VERSION"` + if [ "$LE_VERSION_STATE" = "UNOFFICIAL" ]; then + say "Unofficial certbot-auto version detected, self-upgrade is disabled: $LE_AUTO_VERSION" + elif [ "$LE_VERSION_STATE" = "OUTDATED" ]; then say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." # Now we drop into Python so we don't have to install even more diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index 33f5b2c00..e4902c0ee 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 6d2977832..f431e32e4 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -451,6 +451,35 @@ OldVenvExists() { [ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ] } +# Given python path, version 1 and version 2, check if version 1 is outdated compared to version 2. +# An unofficial version provided as version 1 (eg. 0.28.0.dev0) will be treated +# specifically by printing "UNOFFICIAL". Otherwise, print "OUTDATED" if version 1 +# is outdated, and "UP_TO_DATE" if not. +# This function relies only on installed python environment (2.x or 3.x) by certbot-auto. +CompareVersions() { + "$1" - "$2" "$3" << "UNLIKELY_EOF" +import sys +from distutils.version import StrictVersion + +try: + current = StrictVersion(sys.argv[1]) +except ValueError: + sys.stdout.write('UNOFFICIAL') + sys.exit() + +try: + remote = StrictVersion(sys.argv[2]) +except ValueError: + sys.stdout.write('UP_TO_DATE') + sys.exit() + +if current < remote: + sys.stdout.write('OUTDATED') +else: + sys.stdout.write('UP_TO_DATE') +UNLIKELY_EOF +} + if [ "$1" = "--le-auto-phase2" ]; then # Phase 2: Create venv, install LE, and run. @@ -635,7 +664,12 @@ UNLIKELY_EOF error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates." elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then error "WARNING: unable to check for updates." - elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then + fi + + LE_VERSION_STATE=`CompareVersions "$LE_PYTHON" "$LE_AUTO_VERSION" "$REMOTE_VERSION"` + if [ "$LE_VERSION_STATE" = "UNOFFICIAL" ]; then + say "Unofficial certbot-auto version detected, self-upgrade is disabled: $LE_AUTO_VERSION" + elif [ "$LE_VERSION_STATE" = "OUTDATED" ]; then say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." # Now we drop into Python so we don't have to install even more diff --git a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh index c55527590..3be78d3f8 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh @@ -7,8 +7,7 @@ BootstrapArchCommon() { # - ArchLinux (x86_64) # # "python-virtualenv" is Python3, but "python2-virtualenv" provides - # only "virtualenv2" binary, not "virtualenv" necessary in - # ./tools/_venv_common.py + # only "virtualenv2" binary, not "virtualenv". deps=" python2 diff --git a/letsencrypt-auto-source/pieces/certbot-requirements.txt b/letsencrypt-auto-source/pieces/certbot-requirements.txt index 401a8e25c..08d8d553f 100644 --- a/letsencrypt-auto-source/pieces/certbot-requirements.txt +++ b/letsencrypt-auto-source/pieces/certbot-requirements.txt @@ -1,12 +1,12 @@ -certbot==0.28.0 \ - --hash=sha256:f2f7c816acd695cbcda713a779b0db2b08e9de407146b46e1c4ef5561e0f5417 \ - --hash=sha256:31e3e2ee2a25c009a621c59ac9182f85d937a897c7bd1d47d0e01f3c712a090a -acme==0.28.0 \ - --hash=sha256:d3a564031155fece3f6edce8c5246d4cf34be0977b4c7c5ce84e86c68601c895 \ - --hash=sha256:bf7c2f1c24a26ab5b9fce3a6abca1d74a5914d46919649ae00ad5817db62bb85 -certbot-apache==0.28.0 \ - --hash=sha256:a57d7bac4f13ae5ecea4f4cbd479d381c02316c4832b25ab5a9d7c6826166370 \ - --hash=sha256:3f93f5de4a548e973c493a6cac5eeeb3dbbcae2988b61299ea0727d04a00f5bb -certbot-nginx==0.28.0 \ - --hash=sha256:1822a65910f0801087fa20a3af3fc94f878f93e0f11809483bb5387df861e296 \ - --hash=sha256:426fb403b0a7b203629f4e350a862cbc3bc1f69936fdab8ec7eafe0d8a3b5ddb +certbot==0.30.0 \ + --hash=sha256:b3468e128e74d2295598f6d3fbf9d0edfb67fe5abaca3b985a9e858395bd027f \ + --hash=sha256:d631fe6c75700ce9b2fdae194ff8b53c7518545d87dd451a1704f7572dcd49e8 +acme==0.30.0 \ + --hash=sha256:eed9389f802ebf4988c9e43c28ad3d5c2734237371d78e97450a1d61189a15aa \ + --hash=sha256:984b6d00bec73dcfa616636a760e80ca14bd246fb908710a656547f542f09445 +certbot-apache==0.30.0 \ + --hash=sha256:d38c70fc6930db298ea992a3145362eebdce460d3d2651f86a8f2f43d838c6d0 \ + --hash=sha256:1d4bc207d53a3e5d37e5d9ebd05f26089aa21d1fbf384113ed9d1829b4d1e9bf +certbot-nginx==0.30.0 \ + --hash=sha256:6163c7d0080f59b4ebe510afcc6af2d2eebf15469275c3835866690db4d465d6 \ + --hash=sha256:e39a3f3d77cd4c653949cf066fb2211039fd2032665697c27b6e8501c7c2dd92 diff --git a/letsencrypt-auto-source/pieces/dependency-requirements.txt b/letsencrypt-auto-source/pieces/dependency-requirements.txt index ae6079d96..eb297bc6e 100644 --- a/letsencrypt-auto-source/pieces/dependency-requirements.txt +++ b/letsencrypt-auto-source/pieces/dependency-requirements.txt @@ -21,43 +21,39 @@ pycparser==2.14 \ asn1crypto==0.22.0 \ --hash=sha256:d232509fefcfcdb9a331f37e9c9dc20441019ad927c7d2176cf18ed5da0ba097 \ --hash=sha256:cbbadd640d3165ab24b06ef25d1dca09a3441611ac15f6a6b452474fdf0aed1a -cffi==1.10.0 \ - --hash=sha256:446699c10f3c390633d0722bc19edbc7ac4b94761918a4a4f7908a24e86ebbd0 \ - --hash=sha256:562326fc7f55a59ef3fef5e82908fe938cdc4bbda32d734c424c7cd9ed73e93a \ - --hash=sha256:7f732ad4a30db0b39400c3f7011249f7d0701007d511bf09604729aea222871f \ - --hash=sha256:94fb8410c6c4fc48e7ea759d3d1d9ca561171a88d00faddd4aa0306f698ad6a0 \ - --hash=sha256:587a5043df4b00a2130e09fed42da02a4ed3c688bd9bf07a3ac89d2271f4fb07 \ - --hash=sha256:ec08b88bef627ec1cea210e1608c85d3cf44893bcde74e41b7f7dbdfd2c1bad6 \ - --hash=sha256:a41406f6d62abcdf3eef9fd998d8dcff04fd2a7746644143045feeebd76352d1 \ - --hash=sha256:b560916546b2f209d74b82bdbc3223cee9a165b0242fa00a06dfc48a2054864a \ - --hash=sha256:e74896774e437f4715c57edeb5cf3d3a40d7727f541c2c12156617b5a15d1829 \ - --hash=sha256:9a31c18ba4881a116e448c52f3f5d3e14401cf7a9c43cc88f06f2a7f5428da0e \ - --hash=sha256:80796ea68e11624a0279d3b802f88a7fe7214122b97a15a6c97189934a2cc776 \ - --hash=sha256:f4019826a2dec066c909a1f483ef0dcf9325d6740cc0bd15308942b28b0930f7 \ - --hash=sha256:7248506981eeba23888b4140a69a53c4c0c0a386abcdca61ed8dd790a73e64b9 \ - --hash=sha256:a8955265d146e86fe2ce116394be4eaf0cb40314a79b19f11c4fa574cd639572 \ - --hash=sha256:c49187260043bd4c1d6a52186f9774f17d9b1da0a406798ebf4bfc12da166ade \ - --hash=sha256:c1d8b3d8dcb5c23ac1a8bf56422036f3f305a3c5a8bc8c354256579a1e2aa2c1 \ - --hash=sha256:9e389615bcecb8c782a87939d752340bb0a3a097e90bae54d7f0915bc12f45bd \ - --hash=sha256:d09ff358f75a874f69fa7d1c2b4acecf4282a950293fcfcf89aa606da8a9a500 \ - --hash=sha256:b69b4557aae7de18b7c174a917fe19873529d927ac592762d9771661875bbd40 \ - --hash=sha256:5de52b081a2775e76b971de9d997d85c4457fc0a09079e12d66849548ae60981 \ - --hash=sha256:e7d88fecb7b6250a1fd432e6dc64890342c372fce13dbfe4bb6f16348ad00c14 \ - --hash=sha256:1426e67e855ef7f5030c9184f4f1a9f4bfa020c31c962cd41fd129ec5aef4a6a \ - --hash=sha256:267dd2c66a5760c5f4d47e2ebcf8eeac7ef01e1ae6ae7a6d0d241a290068bc38 \ - --hash=sha256:e553eb489511cacf19eda6e52bc9e151316f0d721724997dda2c4d3079b778db \ - --hash=sha256:98b89b2c57f97ce2db7aeba60db173c84871d73b40e41a11ea95de1500ddc57e \ - --hash=sha256:e2b7e090188833bc58b2ae03fb864c22688654ebd2096bcf38bc860c4f38a3d8 \ - --hash=sha256:afa7d8b8d38ad40db8713ee053d41b36d87d6ae5ec5ad36f9210b548a18dc214 \ - --hash=sha256:4fc9c2ff7924b3a1fa326e1799e5dd58cac585d7fb25fe53ccaa1333b0453d65 \ - --hash=sha256:937db39a1ec5af3003b16357b2042bba67c88d43bc11aaa203fa8a5924524209 \ - --hash=sha256:ab22285797631df3b513b2cd3ecdc51cd8e3d36788e3991d93d0759d6883b027 \ - --hash=sha256:96e599b924ef009aa867f725b3249ee51d76489f484d3a45b4bd219c5ec6ed59 \ - --hash=sha256:bea842a0512be6a8007e585790bccd5d530520fc025ce63b03e139be373b0063 \ - --hash=sha256:e7175287f7fe7b1cc203bb958b17db40abd732690c1e18e700f10e0843a58598 \ - --hash=sha256:285ab352552f52f1398c912556d4d36d4ea9b8450e5c65d03809bf9886755533 \ - --hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \ - --hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5 +cffi==1.11.5 \ + --hash=sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50 \ + --hash=sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596 \ + --hash=sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef \ + --hash=sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743 \ + --hash=sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f \ + --hash=sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31 \ + --hash=sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04 \ + --hash=sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6 \ + --hash=sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3 \ + --hash=sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6 \ + --hash=sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b \ + --hash=sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca \ + --hash=sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e \ + --hash=sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb \ + --hash=sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd \ + --hash=sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1 \ + --hash=sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917 \ + --hash=sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359 \ + --hash=sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f \ + --hash=sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95 \ + --hash=sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801 \ + --hash=sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257 \ + --hash=sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184 \ + --hash=sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc \ + --hash=sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085 \ + --hash=sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93 \ + --hash=sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2 \ + --hash=sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30 \ + --hash=sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5 \ + --hash=sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e \ + --hash=sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b \ + --hash=sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4 ConfigArgParse==0.12.0 \ --hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339 \ --no-binary ConfigArgParse @@ -150,9 +146,9 @@ pytz==2015.7 \ --hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \ --hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \ --hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3 -requests==2.12.1 \ - --hash=sha256:3f3f27a9d0f9092935efc78054ef324eb9f8166718270aefe036dfa1e4f68e1e \ - --hash=sha256:2109ecea94df90980be040490ff1d879971b024861539abb00054062388b612e +requests==2.20.0 \ + --hash=sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c \ + --hash=sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279 six==1.10.0 \ --hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \ --hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a @@ -189,3 +185,12 @@ zope.interface==4.1.3 \ requests-toolbelt==0.8.0 \ --hash=sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237 \ --hash=sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5 +chardet==3.0.2 \ + --hash=sha256:4f7832e7c583348a9eddd927ee8514b3bf717c061f57b21dbe7697211454d9bb \ + --hash=sha256:6ebf56457934fdce01fb5ada5582762a84eed94cad43ed877964aebbdd8174c0 +urllib3==1.24.1 \ + --hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \ + --hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22 +certifi==2017.4.17 \ + --hash=sha256:f4318671072f030a33c7ca6acaef720ddd50ff124d1388e50c1bda4cbd6d7010 \ + --hash=sha256:f7527ebf7461582ce95f7a9e03dd141ce810d40590834f4ec20cddd54234c10a diff --git a/letsencrypt-auto-source/pieces/pipstrap.py b/letsencrypt-auto-source/pieces/pipstrap.py index f21d36657..727040c3c 100755 --- a/letsencrypt-auto-source/pieces/pipstrap.py +++ b/letsencrypt-auto-source/pieces/pipstrap.py @@ -1,12 +1,10 @@ #!/usr/bin/env python """A small script that can act as a trust root for installing pip >=8 - Embed this in your project, and your VCS checkout is all you have to trust. In a post-peep era, this lets you claw your way to a hash-checking version of pip, with which you can install the rest of your dependencies safely. All it assumes is Python 2.6 or better and *some* version of pip already installed. If anything goes wrong, it will exit with a non-zero status code. - """ # This is here so embedded copies are MIT-compliant: # Copyright (c) 2016 Erik Rose @@ -45,7 +43,7 @@ except ImportError: cmd = popenargs[0] raise CalledProcessError(retcode, cmd) return output -from sys import exit, version_info, executable +from sys import exit, version_info from tempfile import mkdtemp try: from urllib2 import build_opener, HTTPHandler, HTTPSHandler @@ -57,7 +55,7 @@ except ImportError: from urllib.parse import urlparse # 3.4 -__version__ = 2, 0, 0 +__version__ = 1, 5, 1 PIP_VERSION = '9.0.1' DEFAULT_INDEX_BASE = 'https://pypi.python.org' @@ -133,10 +131,8 @@ def hashed_download(url, temp, digest): def get_index_base(): """Return the URL to the dir containing the "packages" folder. - Try to wring something out of PIP_INDEX_URL, if set. Hack "/simple" off the end if it's there; that is likely to give us the right dir. - """ env_var = environ.get('PIP_INDEX_URL', '').rstrip('/') if env_var: @@ -150,7 +146,7 @@ def get_index_base(): def main(): - pip_version = StrictVersion(check_output([executable, '-m', 'pip', '--version']) + pip_version = StrictVersion(check_output(['pip', '--version']) .decode('utf-8').split()[1]) min_pip_version = StrictVersion(PIP_VERSION) if pip_version >= min_pip_version: @@ -163,7 +159,7 @@ def main(): temp, digest) for path, digest in PACKAGES] - check_output('{0} -m pip install --no-index --no-deps -U '.format(quote(executable)) + + check_output('pip install --no-index --no-deps -U ' + # Disable cache since we're not using it and it otherwise # sometimes throws permission warnings: ('--no-cache-dir ' if has_pip_cache else '') + @@ -181,4 +177,4 @@ def main(): if __name__ == '__main__': - exit(main()) \ No newline at end of file + exit(main()) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index c5109e208..16c478f20 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -27,6 +27,19 @@ def tests_dir(): return dirname(abspath(__file__)) +def copy_stable(src, dst): + """ + Copy letsencrypt-auto, and replace its current version to its equivalent stable one. + This is needed to test correctly the self-upgrade functionality. + """ + copy(src, dst) + with open(dst, 'r') as file: + filedata = file.read() + filedata = re.sub(r'LE_AUTO_VERSION="(.*)\.dev0"', r'LE_AUTO_VERSION="\1"', filedata) + with open(dst, 'w') as file: + file.write(filedata) + + sys.path.insert(0, dirname(tests_dir())) from build import build as build_le_auto @@ -343,7 +356,7 @@ class AutoTests(TestCase): 'v99.9.9/letsencrypt-auto': build_le_auto(version='99.9.9'), 'v99.9.9/letsencrypt-auto.sig': signed('something else')} with serving(resources) as base_url: - copy(LE_AUTO_PATH, le_auto_path) + copy_stable(LE_AUTO_PATH, le_auto_path) try: out, err = run_le_auto(le_auto_path, venv_dir, base_url) except CalledProcessError as exc: diff --git a/local-oldest-requirements.txt b/local-oldest-requirements.txt index 03226fc84..d582d5c65 100644 --- a/local-oldest-requirements.txt +++ b/local-oldest-requirements.txt @@ -1 +1 @@ -acme[dev]==0.26.0 +acme[dev]==0.29.0 diff --git a/pull_request_template.md b/pull_request_template.md index c071d4135..60fd6da7e 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1 +1,3 @@ -Be sure to edit the `master` section of `CHANGELOG.md` with a line describing this PR before it gets merged. +Be sure to edit the `master` section of `CHANGELOG.md`. This includes a +description of the change and ensuring the modified package(s) are listed as +having been changed. diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..9a5807f34 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,12 @@ +# This file isn't used while testing packages in tools/_release.sh so any +# settings we want to also change there must be added to the release script +# directly. +[pytest] +addopts = --numprocesses auto --pyargs +# ResourceWarnings are ignored as errors, since they're raised at close +# decodestring: https://github.com/rthalley/dnspython/issues/338 +# ignore our own TLS-SNI-01 warning +filterwarnings = + error + ignore:decodestring:DeprecationWarning + ignore:TLS-SNI-01:DeprecationWarning diff --git a/setup.py b/setup.py index f8f5feadc..9e6af2d4f 100644 --- a/setup.py +++ b/setup.py @@ -31,13 +31,13 @@ version = meta['version'] # 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.26.0', + 'acme>=0.29.0', # We technically need ConfigArgParse 0.10.0 for Python 2.6 support, but # saying so here causes a runtime error against our temporary fork of 0.9.3 # in which we added 2.6 support (see #2243), so we relax the requirement. 'ConfigArgParse>=0.9.3', 'configobj', - 'cryptography>=1.2', # load_pem_x509_certificate + 'cryptography>=1.2.3', # load_pem_x509_certificate 'josepy', 'mock', 'parsedatetime>=1.3', # Calendar.parseDT @@ -68,9 +68,10 @@ dev3_extras = [ ] docs_extras = [ + # If you have Sphinx<1.5.1, you need docutils<0.13.1 + # https://github.com/sphinx-doc/sphinx/issues/3212 'repoze.sphinx.autointerface', - # sphinx.ext.imgconverter - 'Sphinx >=1.6', + 'Sphinx>=1.2', # Annotation support 'sphinx_rtd_theme', ] diff --git a/tests/boulder-fetch.sh b/tests/boulder-fetch.sh index 31e0f6b30..f34deb74e 100755 --- a/tests/boulder-fetch.sh +++ b/tests/boulder-fetch.sh @@ -11,7 +11,6 @@ if [ ! -d ${BOULDERPATH} ]; then fi cd ${BOULDERPATH} -sed -i "s/FAKE_DNS: .*/FAKE_DNS: 10.77.77.1/" docker-compose.yml docker-compose up -d boulder @@ -28,3 +27,6 @@ if ! curl http://localhost:4000/directory 2>/dev/null; then echo "timed out waiting for boulder to start" exit 1 fi + +# Setup the DNS resolution used by boulder instance to docker host +curl -X POST -d '{"ip":"10.77.77.1"}' http://localhost:8055/set-default-ipv4 diff --git a/tests/certbot-boulder-integration.sh b/tests/certbot-boulder-integration.sh index 73e668e28..630571148 100755 --- a/tests/certbot-boulder-integration.sh +++ b/tests/certbot-boulder-integration.sh @@ -174,7 +174,7 @@ CheckRenewHook() { TotalAndDistinctLines() { total=$1 distinct=$2 - awk '{a[$1] = 1}; END {exit(NR !='$total' || length(a) !='$distinct')}' + awk '{a[$1] = 1}; END {n = 0; for (i in a) { n++ }; exit(NR !='$total' || n !='$distinct')}' } # Cleanup coverage data @@ -207,10 +207,16 @@ common unregister common register --email ex1@domain.org,ex2@domain.org +# TODO: When `certbot register --update-registration` is fully deprecated, delete the two following deprecated uses + common register --update-registration --email ex1@domain.org common register --update-registration --email ex1@domain.org,ex2@domain.org +common update_account --email example@domain.org + +common update_account --email ex1@domain.org,ex2@domain.org + common plugins --init --prepare | grep webroot # We start a server listening on the port for the @@ -280,7 +286,38 @@ CheckCertCount() { fi } +CheckPermissions() { +# Args: +# Checks mode of two files match under + masked_mode() { echo $((0`stat -c %a $1` & 0$2)); } + if [ `masked_mode $1 $3` -ne `masked_mode $2 $3` ] ; then + echo "With $3 mask, expected mode `masked_mode $1 $3`, got `masked_mode $2 $3` on file $2" + exit 1 + fi +} + +CheckGID() { +# Args: +# Checks group owner of two files match + group_owner() { echo `stat -c %G $1`; } + if [ `group_owner $1` != `group_owner $2` ] ; then + echo "Expected group owner `group_owner $1`, got `group_owner $2` on file $2" + exit 1 + fi +} + +CheckOthersPermission() { +# Args: +# Tests file's other/world permission against expected mode + other_permission=$((0`stat -c %a $1` & 07)) + if [ $other_permission -ne $2 ] ; then + echo "Expected file $1 to have others mode $2, got $other_permission instead" + exit 1 + fi +} + CheckCertCount "le.wtf" 1 + # This won't renew (because it's not time yet) common_no_force_renew renew CheckCertCount "le.wtf" 1 @@ -294,6 +331,12 @@ rm -rf "$renewal_hooks_root" common renew --cert-name le.wtf --authenticator manual CheckCertCount "le.wtf" 2 +CheckOthersPermission "${root}/conf/archive/le.wtf/privkey1.pem" 0 +CheckOthersPermission "${root}/conf/archive/le.wtf/privkey2.pem" 0 +CheckPermissions "${root}/conf/archive/le.wtf/privkey1.pem" "${root}/conf/archive/le.wtf/privkey2.pem" 074 +CheckGID "${root}/conf/archive/le.wtf/privkey1.pem" "${root}/conf/archive/le.wtf/privkey2.pem" +chmod 0444 "${root}/conf/archive/le.wtf/privkey2.pem" + # test renewal with no executables in hook directories for hook_dir in $renewal_hooks_dirs; do touch "$hook_dir/file" @@ -310,6 +353,10 @@ CreateDirHooks sed -i "4arenew_before_expiry = 4 years" "$root/conf/renewal/le.wtf.conf" common_no_force_renew renew --rsa-key-size 2048 --no-directory-hooks CheckCertCount "le.wtf" 3 +CheckGID "${root}/conf/archive/le.wtf/privkey2.pem" "${root}/conf/archive/le.wtf/privkey3.pem" +CheckPermissions "${root}/conf/archive/le.wtf/privkey2.pem" "${root}/conf/archive/le.wtf/privkey3.pem" 074 +CheckOthersPermission "${root}/conf/archive/le.wtf/privkey3.pem" 04 + if [ -s "$HOOK_DIRS_TEST" ]; then echo "Directory hooks were executed with --no-directory-hooks!" >&2 exit 1 diff --git a/tests/certbot-pebble-integration.sh b/tests/certbot-pebble-integration.sh new file mode 100755 index 000000000..8711f72c1 --- /dev/null +++ b/tests/certbot-pebble-integration.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Simple integration test. Make sure to activate virtualenv beforehand +# (source venv/bin/activate) and that you are running Pebble test +# instance (see ./pebble-fetch.sh). + +cleanup_and_exit() { + EXIT_STATUS=$? + unset SERVER + exit $EXIT_STATUS +} + +trap cleanup_and_exit EXIT + +export SERVER=https://localhost:14000/dir + +./tests/certbot-boulder-integration.sh diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 1e444fa26..83aa91a9e 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -3,12 +3,15 @@ root=${root:-$(mktemp -d -t leitXXXX)} echo "Root integration tests directory: $root" config_dir="$root/conf" -store_flags="--config-dir $config_dir --work-dir $root/work" -store_flags="$store_flags --logs-dir $root/logs" tls_sni_01_port=5001 http_01_port=5002 sources="acme/,$(ls -dm certbot*/ | tr -d ' \n')" -export root config_dir store_flags tls_sni_01_port http_01_port sources +export root config_dir tls_sni_01_port http_01_port sources +certbot_path="$(command -v certbot)" +# Flags that are added here will be added to Certbot calls within +# certbot_test_no_force_renew. +other_flags="--config-dir $config_dir --work-dir $root/work" +other_flags="$other_flags --logs-dir $root/logs" certbot_test () { certbot_test_no_force_renew \ @@ -16,11 +19,35 @@ certbot_test () { "$@" } +# Succeeds if Certbot version is at least the given version number and fails +# otherwise. This is useful for making sure Certbot has certain features +# available. The patch version is currently ignored. +# +# Arguments: +# First argument is the minimum major version +# Second argument is the minimum minor version +version_at_least () { + # Certbot major and minor version (e.g. 0.30) + major_minor=$("$certbot_path" --version 2>&1 | cut -d' ' -f2 | cut -d. -f1,2) + major=$(echo "$major_minor" | cut -d. -f1) + minor=$(echo "$major_minor" | cut -d. -f2) + # Test that either the major version is greater or major version is equal + # and minor version is greater than or equal to. + [ \( "$major" -gt "$1" \) -o \( "$major" -eq "$1" -a "$minor" -ge "$2" \) ] +} + # Use local ACMEv2 endpoint if requested and SERVER isn't already set. if [ "${BOULDER_INTEGRATION:-v1}" = "v2" -a -z "${SERVER:+x}" ]; then SERVER="http://localhost:4001/directory" fi +# --no-random-sleep-on-renew was added in +# https://github.com/certbot/certbot/pull/6599 and first released in Certbot +# 0.30.0. +if version_at_least 0 30; then + other_flags="$other_flags --no-random-sleep-on-renew" +fi + certbot_test_no_force_renew () { omit_patterns="*/*.egg-info/*,*/dns_common*,*/setup.py,*/test_*,*/tests/*" omit_patterns="$omit_patterns,*_test.py,*_test_*,certbot-apache/*" @@ -30,13 +57,13 @@ certbot_test_no_force_renew () { --append \ --source $sources \ --omit $omit_patterns \ - $(command -v certbot) \ + "$certbot_path" \ --server "${SERVER:-http://localhost:4000/directory}" \ --no-verify-ssl \ --tls-sni-01-port $tls_sni_01_port \ --http-01-port $http_01_port \ --manual-public-ip-logging-ok \ - $store_flags \ + $other_flags \ --non-interactive \ --no-redirect \ --agree-tos \ diff --git a/tests/letstest/scripts/test_apache2.sh b/tests/letstest/scripts/test_apache2.sh index 4036e6efa..d24de2458 100755 --- a/tests/letstest/scripts/test_apache2.sh +++ b/tests/letstest/scripts/test_apache2.sh @@ -54,6 +54,7 @@ if [ $? -ne 0 ] ; then fi if [ "$OS_TYPE" = "ubuntu" ] ; then + export SERVER="$BOULDER_URL" venv/bin/tox -e apacheconftest else echo Not running hackish apache tests on $OS_TYPE diff --git a/tests/letstest/scripts/test_sdists.sh b/tests/letstest/scripts/test_sdists.sh index f18a64065..0b9a91ffd 100755 --- a/tests/letstest/scripts/test_sdists.sh +++ b/tests/letstest/scripts/test_sdists.sh @@ -10,7 +10,7 @@ VERSION=$(letsencrypt-auto-source/version.py) export VENV_ARGS="-p $PYTHON" # setup venv -tools/_venv_common.sh --requirement letsencrypt-auto-source/pieces/dependency-requirements.txt +tools/_venv_common.py --requirement letsencrypt-auto-source/pieces/dependency-requirements.txt . ./venv/bin/activate # build sdists diff --git a/tests/modification-check.py b/tests/modification-check.py index e00994b04..8abc0fbfe 100755 --- a/tests/modification-check.py +++ b/tests/modification-check.py @@ -70,7 +70,7 @@ def validate_scripts_content(repo_path, temp_cwd): # Compare file against the latest released version latest_version = subprocess.check_output( [sys.executable, 'fetch.py', '--latest-version'], cwd=temp_cwd) - subprocess.call( + subprocess.check_call( [sys.executable, 'fetch.py', '--le-auto-script', 'v{0}'.format(latest_version.decode().strip())], cwd=temp_cwd) if compare_files( @@ -95,7 +95,7 @@ def main(): os.path.normpath(os.path.join(repo_path, 'letsencrypt-auto-source/letsencrypt-auto')), os.path.join(temp_cwd, 'original-lea') ) - subprocess.call([sys.executable, os.path.normpath(os.path.join( + subprocess.check_call([sys.executable, os.path.normpath(os.path.join( repo_path, 'letsencrypt-auto-source/build.py'))]) shutil.copyfile( os.path.normpath(os.path.join(repo_path, 'letsencrypt-auto-source/letsencrypt-auto')), diff --git a/tests/pebble-fetch.sh b/tests/pebble-fetch.sh new file mode 100755 index 000000000..b0ba08961 --- /dev/null +++ b/tests/pebble-fetch.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Download and run Pebble instance for integration testing +set -xe + +PEBBLE_VERSION=2018-11-02 + +# We reuse the same GOPATH-style directory than for Boulder. +# Pebble does not need it, but it will make the installation consistent with Boulder's one. +export GOPATH=${GOPATH:-$HOME/gopath} +PEBBLEPATH=${PEBBLEPATH:-$GOPATH/src/github.com/letsencrypt/pebble} + +mkdir -p ${PEBBLEPATH} + +cat << UNLIKELY_EOF > "$PEBBLEPATH/docker-compose.yml" +version: '3' + +services: + pebble: + image: letsencrypt/pebble:${PEBBLE_VERSION} + command: pebble -strict ${PEBBLE_STRICT:-false} -dnsserver 10.77.77.1 + ports: + - 14000:14000 + environment: + - PEBBLE_VA_NOSLEEP=1 +UNLIKELY_EOF + +docker-compose -f "$PEBBLEPATH/docker-compose.yml" up -d pebble + +set +x # reduce verbosity while waiting for boulder +for n in `seq 1 150` ; do + if curl -k https://localhost:14000/dir 2>/dev/null; then + break + else + sleep 1 + fi +done + +if ! curl -k https://localhost:14000/dir 2>/dev/null; then + echo "timed out waiting for pebble to start" + exit 1 +fi diff --git a/tests/travis-macos-setup.sh b/tests/travis-macos-setup.sh deleted file mode 100755 index bf72f26a5..000000000 --- a/tests/travis-macos-setup.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -e -# -# Set up the test environment for macOS on Travis. - -# Install the given package with brew if it's not already installed. -brew_install() { - if ! brew list "$1" > /dev/null 2>&1; then - brew install "$1" - fi -} - -brew_install augeas -brew_install python -brew_install python3 - -# Ensure we use python from brew. -brew link python diff --git a/tools/_changelog_top.txt b/tools/_changelog_top.txt new file mode 100644 index 000000000..6983b3a43 --- /dev/null +++ b/tools/_changelog_top.txt @@ -0,0 +1,21 @@ +## nextversion - master + +### Added + +* + +### Changed + +* + +### Fixed + +* + +Despite us having broken lockstep, we are continuing to release new versions of +all Certbot components during releases for the time being, however, the only +package with changes other than its version number was: + +* + +More details about these changes can be found on our GitHub repo. diff --git a/tools/_release.sh b/tools/_release.sh index dab4eec3a..ec2deda22 100755 --- a/tools/_release.sh +++ b/tools/_release.sh @@ -65,12 +65,19 @@ if [ "$RELEASE_BRANCH" != "candidate-$version" ] ; then fi git checkout "$RELEASE_BRANCH" +# Update changelog +sed -i "s/master/$(date +'%Y-%m-%d')/" CHANGELOG.md +git add CHANGELOG.md +git diff --cached +git commit -m "Update changelog for $version release" + for pkg_dir in $SUBPKGS_NO_CERTBOT certbot-compatibility-test . do sed -i 's/\.dev0//' "$pkg_dir/setup.py" git add "$pkg_dir/setup.py" done + SetVersion() { ver="$1" # bumping Certbot's version number is done differently @@ -136,7 +143,7 @@ pip install -U pip # (or our dependencies) have conditional dependencies implemented with if # statements in setup.py and we have cached wheels lying around that would # cause those ifs to not be evaluated. -pip install \ +python ../tools/pip_install.py \ --no-cache-dir \ --extra-index-url http://localhost:$PORT \ $SUBPKGS @@ -159,10 +166,11 @@ fi mkdir kgs kgs="kgs/$version" pip freeze | tee $kgs -pip install pytest +python ../tools/pip_install.py pytest for module in $subpkgs_modules ; do echo testing $module - pytest --pyargs $module + # use an empty configuration file rather than the one in the repo root + pytest -c <(echo '') --pyargs $module done cd ~- @@ -232,6 +240,19 @@ echo tar cJvf $name.$rev.tar.xz $name.$rev echo gpg2 -U $RELEASE_GPG_KEY --detach-sign --armor $name.$rev.tar.xz cd ~- +# Add master section to CHANGELOG.md +header=$(head -n 4 CHANGELOG.md) +body=$(sed s/nextversion/$nextversion/ tools/_changelog_top.txt) +footer=$(tail -n +5 CHANGELOG.md) +echo "$header + +$body + +$footer" > CHANGELOG.md +git add CHANGELOG.md +git diff --cached +git commit -m "Add contents to CHANGELOG.md for next version" + echo "New root: $root" echo "Test commands (in the letstest repo):" echo 'python multitester.py targets.yaml $AWS_KEY $USERNAME scripts/test_leauto_upgrades.sh --alt_pip $YOUR_PIP_REPO --branch public-beta' @@ -244,6 +265,16 @@ if [ "$RELEASE_BRANCH" = candidate-"$version" ] ; then SetVersion "$nextversion".dev0 letsencrypt-auto-source/build.py git add letsencrypt-auto-source/letsencrypt-auto + for pkg_dir in $SUBPKGS_NO_CERTBOT . + do + if [ -f "$pkg_dir/local-oldest-requirements.txt" ]; then + sed -i "s/-e acme\[dev\]/acme[dev]==$version/" "$pkg_dir/local-oldest-requirements.txt" + sed -i "s/-e acme/acme[dev]==$version/" "$pkg_dir/local-oldest-requirements.txt" + sed -i "s/-e \.\[dev\]/certbot[dev]==$version/" "$pkg_dir/local-oldest-requirements.txt" + sed -i "s/-e \./certbot[dev]==$version/" "$pkg_dir/local-oldest-requirements.txt" + git add "$pkg_dir/local-oldest-requirements.txt" + fi + done git diff git commit -m "Bump version to $nextversion" fi diff --git a/tools/_venv_common.py b/tools/_venv_common.py index b180518f9..540842773 100755 --- a/tools/_venv_common.py +++ b/tools/_venv_common.py @@ -1,4 +1,14 @@ #!/usr/bin/env python +"""Aids in creating a developer virtual environment for Certbot. + +When this module is run as a script, it takes the arguments that should +be passed to pip to install the Certbot packages as command line +arguments. The virtual environment will be created with the name "venv" +in the current working directory and will use the default version of +Python for the virtualenv executable in your PATH. You can change the +name of the virtual environment by setting the environment variable +VENV_NAME. +""" from __future__ import print_function @@ -8,46 +18,148 @@ import glob import time import subprocess import sys +import re +import shlex -def subprocess_with_print(command): - print(command) - return subprocess.call(command, shell=True) +VERSION_PATTERN = re.compile(r'^(\d+)\.(\d+).*$') -def get_venv_python(venv_path): + +class PythonExecutableNotFoundError(Exception): + pass + + +def find_python_executable(python_major): + # type: (int) -> str + """ + Find the relevant python executable that is of the given python major version. + Will test, in decreasing priority order: + * the current Python interpreter + * 'pythonX' executable in PATH (with X the given major version) if available + * 'python' executable in PATH if available + * Windows Python launcher 'py' executable in PATH if available + Incompatible python versions for Certbot will be evicted (eg. Python < 3.5 on Windows) + :param int python_major: the Python major version to target (2 or 3) + :rtype: str + :return: the relevant python executable path + :raise RuntimeError: if no relevant python executable path could be found + """ + python_executable_path = None + + # First try, current python executable + if _check_version('{0}.{1}.{2}'.format( + sys.version_info[0], sys.version_info[1], sys.version_info[2]), python_major): + return sys.executable + + # Second try, with python executables in path + versions_to_test = ['2.7', '2', ''] if python_major == 2 else ['3', ''] + for one_version in versions_to_test: + try: + one_python = 'python{0}'.format(one_version) + output = subprocess.check_output([one_python, '--version'], + universal_newlines=True, stderr=subprocess.STDOUT) + if _check_version(output.strip().split()[1], python_major): + return subprocess.check_output([one_python, '-c', + 'import sys; sys.stdout.write(sys.executable);'], + universal_newlines=True) + except (subprocess.CalledProcessError, OSError): + pass + + # Last try, with Windows Python launcher + try: + env_arg = '-{0}'.format(python_major) + output_version = subprocess.check_output(['py', env_arg, '--version'], + universal_newlines=True, stderr=subprocess.STDOUT) + if _check_version(output_version.strip().split()[1], python_major): + return subprocess.check_output(['py', env_arg, '-c', + 'import sys; sys.stdout.write(sys.executable);'], + universal_newlines=True) + except (subprocess.CalledProcessError, OSError): + pass + + if not python_executable_path: + raise RuntimeError('Error, no compatible Python {0} executable for Certbot could be found.' + .format(python_major)) + + +def _check_version(version_str, major_version): + search = VERSION_PATTERN.search(version_str) + + if not search: + return False + + version = (int(search.group(1)), int(search.group(2))) + + minimal_version_supported = (2, 7) + if major_version == 3 and os.name == 'nt': + minimal_version_supported = (3, 5) + elif major_version == 3: + minimal_version_supported = (3, 4) + + if version >= minimal_version_supported: + return True + + print('Incompatible python version for Certbot found: {0}'.format(version_str)) + return False + + +def subprocess_with_print(cmd, env=os.environ, shell=False): + print('+ {0}'.format(subprocess.list2cmdline(cmd)) if isinstance(cmd, list) else cmd) + subprocess.check_call(cmd, env=env, shell=shell) + + +def get_venv_bin_path(venv_path): python_linux = os.path.join(venv_path, 'bin/python') - python_windows = os.path.join(venv_path, 'Scripts\\python.exe') if os.path.isfile(python_linux): - return python_linux + return os.path.abspath(os.path.dirname(python_linux)) + python_windows = os.path.join(venv_path, 'Scripts\\python.exe') if os.path.isfile(python_windows): - return python_windows + return os.path.abspath(os.path.dirname(python_windows)) raise ValueError(( 'Error, could not find python executable in venv path {0}: is it a valid venv ?' .format(venv_path))) + def main(venv_name, venv_args, args): + """Creates a virtual environment and installs packages. + + :param str venv_name: The name or path at where the virtual + environment should be created. + :param str venv_args: Command line arguments for virtualenv + :param str args: Command line arguments that should be given to pip + to install packages + """ + for path in glob.glob('*.egg-info'): if os.path.isdir(path): shutil.rmtree(path) else: os.remove(path) + env_venv_name = os.environ.get('VENV_NAME') + if env_venv_name: + print('Creating venv at {0}' + ' as specified in VENV_NAME'.format(env_venv_name)) + venv_name = env_venv_name + if os.path.isdir(venv_name): os.rename(venv_name, '{0}.{1}.bak'.format(venv_name, int(time.time()))) - exit_code = 0 + command = [sys.executable, '-m', 'virtualenv', '--no-site-packages', '--setuptools', venv_name] + command.extend(shlex.split(venv_args)) + subprocess_with_print(command) - exit_code = subprocess_with_print(' '.join([ - sys.executable, '-m', 'virtualenv', '--no-site-packages', '--setuptools', - venv_name, venv_args])) or exit_code - - python_executable = get_venv_python(venv_name) - - exit_code = subprocess_with_print(' '.join([ - python_executable, os.path.normpath('./letsencrypt-auto-source/pieces/pipstrap.py')])) - command = [python_executable, os.path.normpath('./tools/pip_install.py')] or exit_code - command.extend(args) - exit_code = subprocess_with_print(' '.join(command)) or exit_code + # We execute the following commands in the context of the virtual environment, to install + # the packages in it. To do so, we append the venv binary to the PATH that will be used for + # these commands. With this trick, correct python executable will be selected. + new_environ = os.environ.copy() + new_environ['PATH'] = os.pathsep.join([get_venv_bin_path(venv_name), new_environ['PATH']]) + subprocess_with_print('python {0}'.format('./letsencrypt-auto-source/pieces/pipstrap.py'), + env=new_environ, shell=True) + subprocess_with_print('python -m pip install --upgrade "setuptools>=30.3"', + env=new_environ, shell=True) + subprocess_with_print('python {0} {1}'.format('./tools/pip_install.py', ' '.join(args)), + env=new_environ, shell=True) if os.path.isdir(os.path.join(venv_name, 'bin')): # Linux/OSX specific @@ -59,15 +171,14 @@ def main(venv_name, venv_args, args): # Windows specific print('---------------------------------------------------------------------------') print('Please run one of the following commands to activate developer environment:') - print('{0}\\bin\\activate.bat (for Batch)'.format(venv_name)) + print('{0}\\Scripts\\activate.bat (for Batch)'.format(venv_name)) print('.\\{0}\\Scripts\\Activate.ps1 (for Powershell)'.format(venv_name)) print('---------------------------------------------------------------------------') else: raise ValueError('Error, directory {0} is not a valid venv.'.format(venv_name)) - return exit_code if __name__ == '__main__': - sys.exit(main(os.environ.get('VENV_NAME', 'venv'), - os.environ.get('VENV_ARGS', ''), - sys.argv[1:])) + main('venv', + '', + sys.argv[1:]) diff --git a/tools/dev_constraints.txt b/tools/dev_constraints.txt index 380d49cb3..778012d31 100644 --- a/tools/dev_constraints.txt +++ b/tools/dev_constraints.txt @@ -7,14 +7,14 @@ astroid==1.3.5 attrs==17.3.0 Babel==2.5.1 backports.shutil-get-terminal-size==1.0.0 -boto3==1.4.7 -botocore==1.7.41 +boto3==1.9.36 +botocore==1.12.36 cloudflare==1.5.1 coverage==4.4.2 decorator==4.1.2 dns-lexicon==2.7.14 dnspython==1.15.0 -docutils==0.14 +docutils==0.12 execnet==1.5.0 future==0.16.0 futures==3.1.1 diff --git a/tools/install_and_test.py b/tools/install_and_test.py index 149ffc776..79a7c2264 100755 --- a/tools/install_and_test.py +++ b/tools/install_and_test.py @@ -19,11 +19,11 @@ SKIP_PROJECTS_ON_WINDOWS = [ def call_with_print(command, cwd=None): print(command) - return subprocess.call(command, shell=True, cwd=cwd or os.getcwd()) + subprocess.check_call(command, shell=True, cwd=cwd or os.getcwd()) def main(args): if os.environ.get('CERTBOT_NO_PIN') == '1': - command = [sys.executable, '-m', 'pip', '-q', '-e'] + command = [sys.executable, '-m', 'pip', '-e'] else: script_dir = os.path.dirname(os.path.abspath(__file__)) command = [sys.executable, os.path.join(script_dir, 'pip_install_editable.py')] @@ -37,26 +37,22 @@ def main(args): else: new_args.append(arg) - exit_code = 0 - for requirement in new_args: current_command = command[:] current_command.append(requirement) - exit_code = call_with_print(' '.join(current_command)) or exit_code + call_with_print(' '.join(current_command)) pkg = re.sub(r'\[\w+\]', '', requirement) if pkg == '.': pkg = 'certbot' temp_cwd = tempfile.mkdtemp() + shutil.copy2("pytest.ini", temp_cwd) try: - exit_code = call_with_print(' '.join([ - sys.executable, '-m', 'pytest', '--numprocesses', 'auto', - '--quiet', '--pyargs', pkg.replace('-', '_')]), cwd=temp_cwd) or exit_code + call_with_print(' '.join([ + sys.executable, '-m', 'pytest', pkg.replace('-', '_')]), cwd=temp_cwd) finally: shutil.rmtree(temp_cwd) - return exit_code - if __name__ == '__main__': - sys.exit(main(sys.argv[1:])) + main(sys.argv[1:]) diff --git a/tools/oldest_constraints.txt b/tools/oldest_constraints.txt index de2b83ad8..20fa0672a 100644 --- a/tools/oldest_constraints.txt +++ b/tools/oldest_constraints.txt @@ -37,15 +37,21 @@ pytz==2012rc0 # Our setup.py constraints cloudflare==1.5.1 -cryptography==1.2.0 +cryptography==1.2.3 google-api-python-client==1.5 oauth2client==2.0 parsedatetime==1.3 pyparsing==1.5.5 python-digitalocean==1.11 -requests[security]==2.4.1 +requests[security]==2.6.0 # Ubuntu Xenial constraints ConfigArgParse==0.10.0 funcsigs==0.4 zope.hookable==4.0.4 + +# Plugin constraints +# These aren't necessarily the oldest versions we need to support +# Tracking at https://github.com/certbot/certbot/issues/6473 +boto3==1.4.7 +botocore==1.7.41 diff --git a/tools/pip_install.py b/tools/pip_install.py index 273ce5ec2..4466729e0 100755 --- a/tools/pip_install.py +++ b/tools/pip_install.py @@ -20,9 +20,11 @@ import tempfile import merge_requirements as merge_module import readlink + def find_tools_path(): return os.path.dirname(readlink.main(__file__)) + def certbot_oldest_processing(tools_path, args, test_constraints): if args[0] != '-e' or len(args) != 2: raise ValueError('When CERTBOT_OLDEST is set, this script must be run ' @@ -37,6 +39,7 @@ def certbot_oldest_processing(tools_path, args, test_constraints): return requirements + def certbot_normal_processing(tools_path, test_constraints): repo_path = os.path.dirname(tools_path) certbot_requirements = os.path.normpath(os.path.join( @@ -49,6 +52,7 @@ def certbot_normal_processing(tools_path, test_constraints): if search: fd.write('{0}{1}'.format(search.group(1), os.linesep)) + def merge_requirements(tools_path, test_constraints, all_constraints): merged_requirements = merge_module.main( os.path.join(tools_path, 'dev_constraints.txt'), @@ -57,15 +61,20 @@ def merge_requirements(tools_path, test_constraints, all_constraints): with open(all_constraints, 'w') as fd: fd.write(merged_requirements) + def call_with_print(command, cwd=None): print(command) - return subprocess.call(command, shell=True, cwd=cwd or os.getcwd()) + subprocess.check_call(command, shell=True, cwd=cwd or os.getcwd()) + def main(args): tools_path = find_tools_path() working_dir = tempfile.mkdtemp() - exit_code = 0 + if os.environ.get('TRAVIS'): + # When this script is executed on Travis, the following print will make the log + # be folded until the end command is printed (see finally section). + print('travis_fold:start:install_certbot_deps') try: test_constraints = os.path.join(working_dir, 'test_constraints.txt') @@ -79,17 +88,16 @@ def main(args): merge_requirements(tools_path, test_constraints, all_constraints) if requirements: - exit_code = call_with_print(' '.join([ - sys.executable, '-m', 'pip', 'install', '-q', '--constraint', all_constraints, - '--requirement', requirements])) or exit_code + call_with_print('"{0}" -m pip install --constraint "{1}" --requirement "{2}"' + .format(sys.executable, all_constraints, requirements)) - command = [sys.executable, '-m', 'pip', 'install', '-q', '--constraint', all_constraints] - command.extend(args) - exit_code = call_with_print(' '.join(command)) or exit_code + call_with_print('"{0}" -m pip install --constraint "{1}" {2}' + .format(sys.executable, all_constraints, ' '.join(args))) finally: + if os.environ.get('TRAVIS'): + print('travis_fold:end:install_certbot_deps') shutil.rmtree(working_dir) - return exit_code if __name__ == '__main__': - sys.exit(main(sys.argv[1:])) + main(sys.argv[1:]) diff --git a/tools/pip_install_editable.py b/tools/pip_install_editable.py index 35cc2264d..8eaf3a9fa 100755 --- a/tools/pip_install_editable.py +++ b/tools/pip_install_editable.py @@ -14,7 +14,7 @@ def main(args): new_args.append('-e') new_args.append(arg) - return pip_install.main(new_args) + pip_install.main(new_args) if __name__ == '__main__': - sys.exit(main(sys.argv[1:])) + main(sys.argv[1:]) diff --git a/tools/venv.py b/tools/venv.py index 2cc43251d..93b012e76 100755 --- a/tools/venv.py +++ b/tools/venv.py @@ -1,11 +1,6 @@ #!/usr/bin/env python # Developer virtualenv setup for Certbot client - -from __future__ import absolute_import - import os -import subprocess -import sys import _venv_common @@ -33,27 +28,14 @@ REQUIREMENTS = [ '-e certbot-compatibility-test', ] -def get_venv_args(): - with open(os.devnull, 'w') as fnull: - command_python2_st_code = subprocess.call( - 'command -v python2', shell=True, stdout=fnull, stderr=fnull) - if not command_python2_st_code: - return '--python python2' - - command_python27_st_code = subprocess.call( - 'command -v python2.7', shell=True, stdout=fnull, stderr=fnull) - if not command_python27_st_code: - return '--python python2.7' - - raise ValueError('Couldn\'t find python2 or python2.7 in {0}'.format(os.environ.get('PATH'))) def main(): if os.name == 'nt': raise ValueError('Certbot for Windows is not supported on Python 2.x.') - venv_args = get_venv_args() + venv_args = '--python "{0}"'.format(_venv_common.find_python_executable(2)) + _venv_common.main('venv', venv_args, REQUIREMENTS) - return _venv_common.main('venv', venv_args, REQUIREMENTS) if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/tools/venv3.py b/tools/venv3.py index 1bacc9c9a..c2374ba5a 100755 --- a/tools/venv3.py +++ b/tools/venv3.py @@ -1,12 +1,5 @@ #!/usr/bin/env python # Developer virtualenv setup for Certbot client - -from __future__ import absolute_import - -import os -import subprocess -import sys - import _venv_common REQUIREMENTS = [ @@ -33,22 +26,11 @@ REQUIREMENTS = [ '-e certbot-compatibility-test', ] -def get_venv_args(): - with open(os.devnull, 'w') as fnull: - where_python3_st_code = subprocess.call( - 'where python3', shell=True, stdout=fnull, stderr=fnull) - command_python3_st_code = subprocess.call( - 'command -v python3', shell=True, stdout=fnull, stderr=fnull) - - if not where_python3_st_code or not command_python3_st_code: - return '--python python3' - - raise ValueError('Couldn\'t find python3 in {0}'.format(os.environ.get('PATH'))) def main(): - venv_args = get_venv_args() + venv_args = '--python "{0}"'.format(_venv_common.find_python_executable(3)) + _venv_common.main('venv3', venv_args, REQUIREMENTS) - return _venv_common.main('venv3', venv_args, REQUIREMENTS) if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/tox.cover.py b/tox.cover.py index 8bbce2d09..c8a45d82d 100755 --- a/tox.cover.py +++ b/tox.cover.py @@ -12,36 +12,33 @@ DEFAULT_PACKAGES = [ 'certbot_dns_sakuracloud', 'certbot_nginx', 'certbot_postfix', 'letshelp_certbot'] COVER_THRESHOLDS = { - 'certbot': 98, - 'acme': 100, - 'certbot_apache': 100, - 'certbot_dns_cloudflare': 98, - 'certbot_dns_cloudxns': 99, - 'certbot_dns_digitalocean': 98, - 'certbot_dns_dnsimple': 98, - 'certbot_dns_dnsmadeeasy': 99, - 'certbot_dns_gehirn': 97, - 'certbot_dns_google': 99, - 'certbot_dns_linode': 98, - 'certbot_dns_luadns': 98, - 'certbot_dns_nsone': 99, - 'certbot_dns_ovh': 97, - 'certbot_dns_rfc2136': 99, - 'certbot_dns_route53': 92, - 'certbot_dns_sakuracloud': 97, - 'certbot_nginx': 97, - 'certbot_postfix': 100, - 'letshelp_certbot': 100 + 'certbot': {'linux': 98, 'windows': 94}, + 'acme': {'linux': 100, 'windows': 99}, + 'certbot_apache': {'linux': 100, 'windows': 100}, + 'certbot_dns_cloudflare': {'linux': 98, 'windows': 98}, + 'certbot_dns_cloudxns': {'linux': 99, 'windows': 99}, + 'certbot_dns_digitalocean': {'linux': 98, 'windows': 98}, + 'certbot_dns_dnsimple': {'linux': 98, 'windows': 98}, + 'certbot_dns_dnsmadeeasy': {'linux': 99, 'windows': 99}, + 'certbot_dns_gehirn': {'linux': 97, 'windows': 97}, + 'certbot_dns_google': {'linux': 99, 'windows': 99}, + 'certbot_dns_linode': {'linux': 98, 'windows': 98}, + 'certbot_dns_luadns': {'linux': 98, 'windows': 98}, + 'certbot_dns_nsone': {'linux': 99, 'windows': 99}, + 'certbot_dns_ovh': {'linux': 97, 'windows': 97}, + 'certbot_dns_rfc2136': {'linux': 99, 'windows': 99}, + 'certbot_dns_route53': {'linux': 92, 'windows': 92}, + 'certbot_dns_sakuracloud': {'linux': 97, 'windows': 97}, + 'certbot_nginx': {'linux': 97, 'windows': 97}, + 'certbot_postfix': {'linux': 100, 'windows': 100}, + 'letshelp_certbot': {'linux': 100, 'windows': 100} } SKIP_PROJECTS_ON_WINDOWS = [ 'certbot-apache', 'certbot-nginx', 'certbot-postfix', 'letshelp-certbot'] def cover(package): - threshold = COVER_THRESHOLDS.get(package) - - if not threshold: - raise ValueError('Unrecognized package: {0}'.format(package)) + threshold = COVER_THRESHOLDS.get(package)['windows' if os.name == 'nt' else 'linux'] pkg_dir = package.replace('_', '-') @@ -51,10 +48,9 @@ def cover(package): .format(pkg_dir))) return - subprocess.call([ - sys.executable, '-m', 'pytest', '--cov', pkg_dir, '--cov-append', '--cov-report=', - '--numprocesses', 'auto', '--pyargs', package]) - subprocess.call([ + subprocess.check_call([ + sys.executable, '-m', 'pytest', '--cov', pkg_dir, '--cov-append', '--cov-report=', package]) + subprocess.check_call([ sys.executable, '-m', 'coverage', 'report', '--fail-under', str(threshold), '--include', '{0}/*'.format(pkg_dir), '--show-missing']) diff --git a/tox.ini b/tox.ini index e38f1311e..95b2f2c64 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ [tox] skipsdist = true -envlist = modification,py{34,35,36},py27-cover,lint +envlist = modification,py3,py27-cover,lint,mypy [base] # pip installs the requested packages in editable mode @@ -64,9 +64,6 @@ source_paths = tests/lock_test.py [testenv] -passenv = - TRAVIS - APPVEYOR commands = {[base]install_and_test} {[base]all_packages} python tests/lock_test.py @@ -155,6 +152,20 @@ commands = commands = {[base]pip_install} acme . certbot-apache certbot-compatibility-test {toxinidir}/certbot-apache/certbot_apache/tests/apache-conf-files/apache-conf-test --debian-modules +passenv = + SERVER + +[testenv:apacheconftest-with-pebble] +commands = + {toxinidir}/tests/pebble-fetch.sh + {[testenv:apacheconftest]commands} +passenv = + HOME + GOPATH + PEBBLEPATH + PEBBLE_STRICT +setenv = + SERVER=https://localhost:14000/dir [testenv:nginxroundtrip] commands = @@ -176,7 +187,6 @@ whitelist_externals = docker passenv = DOCKER_* - TRAVIS [testenv:nginx_compat] commands = @@ -187,7 +197,6 @@ whitelist_externals = docker passenv = DOCKER_* - TRAVIS [testenv:le_auto_precise] # At the moment, this tests under Python 2.7 only, as only that version is @@ -199,7 +208,6 @@ whitelist_externals = docker passenv = DOCKER_* - TRAVIS [testenv:le_auto_trusty] # At the moment, this tests under Python 2.7 only, as only that version is @@ -212,7 +220,6 @@ whitelist_externals = docker passenv = DOCKER_* - TRAVIS TRAVIS_BRANCH [testenv:le_auto_wheezy] @@ -241,5 +248,5 @@ passenv = DOCKER_* commands = docker-compose run --rm --service-ports development bash -c 'tox -e lint' whitelist_externals = - docker + docker-compose passenv = DOCKER_*