From ae7967c8aed28a8416a329e5eeac117c1672c878 Mon Sep 17 00:00:00 2001 From: alexzorin Date: Wed, 20 Jul 2022 09:17:27 +1000 Subject: [PATCH 01/29] docs: how to override the trusted CA certificates (#9357) * docs: how to override the trusted CA certificates * Update certbot/docs/using.rst Co-authored-by: ohemorange Co-authored-by: ohemorange --- certbot/certbot/configuration.py | 6 +++++- certbot/docs/using.rst | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/certbot/certbot/configuration.py b/certbot/certbot/configuration.py index d5ad87599..dd40a096f 100644 --- a/certbot/certbot/configuration.py +++ b/certbot/certbot/configuration.py @@ -170,7 +170,11 @@ class NamespaceConfig: @property def no_verify_ssl(self) -> bool: - """Disable verification of the ACME server's certificate.""" + """Disable verification of the ACME server's certificate. + + The root certificates trusted by Certbot can be overriden by setting the + REQUESTS_CA_BUNDLE environment variable. + """ return self.namespace.no_verify_ssl @property diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst index 0038d1f83..c4f55bc02 100644 --- a/certbot/docs/using.rst +++ b/certbot/docs/using.rst @@ -1078,6 +1078,12 @@ ACME directory. For example, if you would like to use Let's Encrypt's staging server, you would add ``--server https://acme-staging-v02.api.letsencrypt.org/directory`` to the command line. +If Certbot does not trust the SSL certificate used by the ACME server, you +can use the `REQUESTS_CA_BUNDLE +`_ +environment variable to override the root certificates trusted by Certbot. Certbot +uses the ``requests`` library, which does not use the operating system trusted root store. + If you use ``--server`` to specify an ACME CA that implements the standardized version of the spec, you may be able to obtain a certificate for a wildcard domain. Some CAs (such as Let's Encrypt) require that domain From f82530d8c0b8744081b2c2ea838c355906247a09 Mon Sep 17 00:00:00 2001 From: alexzorin Date: Tue, 26 Jul 2022 06:43:49 +1000 Subject: [PATCH 02/29] letstest: replace ubuntu 21.10 with 22.04 (#9364) as ubuntu 21.10 is now EOL --- letstest/targets/targets.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letstest/targets/targets.yaml b/letstest/targets/targets.yaml index 1812b3645..a11452f32 100644 --- a/letstest/targets/targets.yaml +++ b/letstest/targets/targets.yaml @@ -6,8 +6,8 @@ targets: #----------------------------------------------------------------------------- #Ubuntu - - ami: ami-0c2d5393cb5b518f6 - name: ubuntu21.10 + - ami: ami-051dcca84f1edfff1 + name: ubuntu22.04 type: ubuntu virt: hvm user: ubuntu From 495b97aafe3d4b2f7c9f7919df401cc20bd4ba50 Mon Sep 17 00:00:00 2001 From: Preston Locke Date: Tue, 26 Jul 2022 18:03:53 -0500 Subject: [PATCH 03/29] Clarify in docs that deletion does not revoke (#9348) * Clarify in docs that deletion does not revoke * Add myself to AUTHORS.md * Move new paragraph below first note and change its wording --- AUTHORS.md | 1 + certbot/docs/using.rst | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/AUTHORS.md b/AUTHORS.md index 95e63459b..64f60a278 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -215,6 +215,7 @@ Authors * [Pierre Jaury](https://github.com/kaiyou) * [Piotr Kasprzyk](https://github.com/kwadrat) * [Prayag Verma](https://github.com/pra85) +* [Preston Locke](https://github.com/Preston12321) * [Rasesh Patel](https://github.com/raspat1) * [Reinaldo de Souza Jr](https://github.com/juniorz) * [Remi Rampin](https://github.com/remram44) diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst index c4f55bc02..38aaf4ff4 100644 --- a/certbot/docs/using.rst +++ b/certbot/docs/using.rst @@ -558,6 +558,11 @@ If you need to delete a certificate, use the ``delete`` subcommand. .. note:: Read this and the `Safely deleting certificates`_ sections carefully. This is an irreversible operation and must be done with care. +Certbot does not automatically revoke a certificate before deleting it. If you're no longer using a certificate and don't +plan to use it anywhere else, you may want to follow the instructions in `Revoking certificates`_ instead. Generally, there's +no need to revoke a certificate if its private key has not been compromised, but you may still receive expiration emails +from Let's Encrypt unless you revoke. + .. note:: Do not manually delete certificate files from inside ``/etc/letsencrypt/``. Always use the ``delete`` subcommand. A certificate may be deleted by providing its name with ``--cert-name``. \ From e9e7a69c7bb5cdac8215692d4bda9a661c0189c1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 28 Jul 2022 13:28:36 -0700 Subject: [PATCH 04/29] Update Azure Docker docs (#9363) * describe docker access token more * Remove extra spaces Co-authored-by: ohemorange Co-authored-by: ohemorange --- .../templates/stages/deploy-stage.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.azure-pipelines/templates/stages/deploy-stage.yml b/.azure-pipelines/templates/stages/deploy-stage.yml index 88aeab60e..424ffed10 100644 --- a/.azure-pipelines/templates/stages/deploy-stage.yml +++ b/.azure-pipelines/templates/stages/deploy-stage.yml @@ -96,11 +96,16 @@ stages: # which was created by following the instructions at # https://docs.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml#sep-docreg. # The name given to this service account must match the value - # given to containerRegistry below. "Grant access to all - # pipelines" should also be checked. To revoke these - # credentials, we can change the password on the certbotbot - # Docker Hub account or remove the account from the - # Certbot organization on Docker Hub. + # given to containerRegistry below. The authentication used when + # creating this service account was a personal access token + # rather than a password to bypass 2FA. When Brad set this up, + # Azure Pipelines failed to verify the credentials with an error + # like "access is forbidden with a JWT issued from a personal + # access token", but after saving them without verification, the + # access token worked when the pipeline actually ran. "Grant + # access to all pipelines" should also be checked on the service + # account. The access token can be deleted on Docker Hub if + # these credentials need to be revoked. containerRegistry: docker-hub displayName: Login to Docker Hub - bash: set -e && tools/docker/deploy.sh $(dockerTag) $DOCKER_ARCH From 42a4d30267c731b80fe73c1d38add046d0bd0899 Mon Sep 17 00:00:00 2001 From: alexzorin Date: Fri, 29 Jul 2022 10:26:12 +1000 Subject: [PATCH 05/29] deps: remove pyjwt dependency (#9337) * deps: remove pyjwt dependency * pinning: strip extras from dependencies `poetry export` outputs in requirements.txt format, which is now apparently producing "dep[extra]==...". We are using this output as the constraints file for pip and pip's new resolver does not permit extras in the constraints file. This change filters out the extras specifiers. * repin current dependencies * fix new pylint complaints * silence lint about distutils.version We have already deprecated the function and it'll be removed in 2.0. * docs: set sphinx language to 'en' this is emitting a warning and failing the build * Revert "pinning: strip extras from dependencies" This reverts commit 11268fd23160ac53fd8dad7a2ff15e453678e159. * pin poetry back to avoid extras issue * repin * fix new mypy complaints in acme/ --- acme/acme/challenges.py | 2 +- acme/acme/crypto_util.py | 20 +- acme/acme/standalone.py | 17 +- acme/tests/challenges_test.py | 4 +- .../certbot_tests/assertions.py | 4 +- .../utils/acme_server.py | 15 +- .../_internal/dns_route53.py | 3 +- certbot/certbot/_internal/lock.py | 6 +- certbot/certbot/achallenges.py | 4 +- certbot/certbot/plugins/dns_test_common.py | 2 - .../plugins/dns_test_common_lexicon.py | 1 - certbot/certbot/util.py | 4 +- certbot/docs/conf.py | 2 +- tools/pinning/current/pyproject.toml | 9 +- tools/requirements.txt | 275 +++++++++--------- 15 files changed, 188 insertions(+), 180 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 9000b370a..91c3e6f24 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -408,7 +408,7 @@ class TLSALPN01Response(KeyAuthorizationChallengeResponse): """ ID_PE_ACME_IDENTIFIER_V1 = b"1.3.6.1.5.5.7.1.30.1" - ACME_TLS_1_PROTOCOL = "acme-tls/1" + ACME_TLS_1_PROTOCOL = b"acme-tls/1" @property def h(self) -> bytes: diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index 956366469..754f70e46 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -11,6 +11,7 @@ from typing import Callable from typing import List from typing import Mapping from typing import Optional +from typing import Sequence from typing import Set from typing import Tuple from typing import Union @@ -39,7 +40,9 @@ class _DefaultCertSelection: def __call__(self, connection: SSL.Connection) -> Optional[Tuple[crypto.PKey, crypto.X509]]: server_name = connection.get_servername() - return self.certs.get(server_name, None) + if server_name: + return self.certs.get(server_name, None) + return None # pragma: no cover class SSLSocket: # pylint: disable=too-few-public-methods @@ -60,7 +63,8 @@ class SSLSocket: # pylint: disable=too-few-public-methods method: int = _DEFAULT_SSL_METHOD, alpn_selection: Optional[Callable[[SSL.Connection, List[bytes]], bytes]] = None, cert_selection: Optional[Callable[[SSL.Connection], - Tuple[crypto.PKey, crypto.X509]]] = None + Optional[Tuple[crypto.PKey, + crypto.X509]]]] = None ) -> None: self.sock = sock self.alpn_selection = alpn_selection @@ -71,8 +75,8 @@ class SSLSocket: # pylint: disable=too-few-public-methods raise ValueError("Both cert_selection and certs specified.") actual_cert_selection: Union[_DefaultCertSelection, Optional[Callable[[SSL.Connection], - Tuple[crypto.PKey, - crypto.X509]]]] = cert_selection + Optional[Tuple[crypto.PKey, + crypto.X509]]]]] = cert_selection if actual_cert_selection is None: actual_cert_selection = _DefaultCertSelection(certs if certs else {}) self.cert_selection = actual_cert_selection @@ -157,7 +161,7 @@ class SSLSocket: # pylint: disable=too-few-public-methods def probe_sni(name: bytes, host: bytes, port: int = 443, timeout: int = 300, # pylint: disable=too-many-arguments method: int = _DEFAULT_SSL_METHOD, source_address: Tuple[str, int] = ('', 0), - alpn_protocols: Optional[List[str]] = None) -> crypto.X509: + alpn_protocols: Optional[Sequence[bytes]] = None) -> crypto.X509: """Probe SNI server for SSL certificate. :param bytes name: Byte string to send as the server name in the @@ -170,7 +174,7 @@ def probe_sni(name: bytes, host: bytes, port: int = 443, timeout: int = 300, # of source interface). See `socket.creation_connection` for more info. Available only in Python 2.7+. :param alpn_protocols: Protocols to request using ALPN. - :type alpn_protocols: `list` of `str` + :type alpn_protocols: `Sequence` of `bytes` :raises acme.errors.Error: In case of any problems. @@ -207,7 +211,9 @@ def probe_sni(name: bytes, host: bytes, port: int = 443, timeout: int = 300, # client_ssl.shutdown() except SSL.Error as error: raise errors.Error(error) - return client_ssl.get_peer_certificate() + cert = client_ssl.get_peer_certificate() + assert cert # Appease mypy. We would have crashed out by now if there was no certificate. + return cert def make_csr(private_key_pem: bytes, domains: Optional[Union[Set[str], List[str]]] = None, diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py index a23f6d603..62174813f 100644 --- a/acme/acme/standalone.py +++ b/acme/acme/standalone.py @@ -46,10 +46,12 @@ class TLSServer(socketserver.TCPServer): method=self.method)) def _cert_selection(self, connection: SSL.Connection - ) -> Tuple[crypto.PKey, crypto.X509]: # pragma: no cover + ) -> Optional[Tuple[crypto.PKey, crypto.X509]]: # pragma: no cover """Callback selecting certificate for connection.""" server_name = connection.get_servername() - return self.certs.get(server_name, None) + if server_name: + return self.certs.get(server_name, None) + return None def server_bind(self) -> None: self._wrap_sock() @@ -151,7 +153,7 @@ class TLSALPN01Server(TLSServer, ACMEServerMixin): def __init__(self, server_address: Tuple[str, int], certs: List[Tuple[crypto.PKey, crypto.X509]], - challenge_certs: Mapping[str, Tuple[crypto.PKey, crypto.X509]], + challenge_certs: Mapping[bytes, Tuple[crypto.PKey, crypto.X509]], ipv6: bool = False) -> None: # We don't need to implement a request handler here because the work # (including logging) is being done by wrapped socket set up in the @@ -161,7 +163,8 @@ class TLSALPN01Server(TLSServer, ACMEServerMixin): ipv6=ipv6) self.challenge_certs = challenge_certs - def _cert_selection(self, connection: SSL.Connection) -> Tuple[crypto.PKey, crypto.X509]: + def _cert_selection(self, connection: SSL.Connection) -> Optional[Tuple[crypto.PKey, + crypto.X509]]: # TODO: We would like to serve challenge cert only if asked for it via # ALPN. To do this, we need to retrieve the list of protos from client # hello, but this is currently impossible with openssl [0], and ALPN @@ -170,8 +173,10 @@ class TLSALPN01Server(TLSServer, ACMEServerMixin): # handshake in alpn_selection() if ALPN protos are not what we expect. # [0] https://github.com/openssl/openssl/issues/4952 server_name = connection.get_servername() - logger.debug("Serving challenge cert for server name %s", server_name) - return self.challenge_certs[server_name] + if server_name: + logger.debug("Serving challenge cert for server name %s", server_name) + return self.challenge_certs[server_name] + return None # pragma: no cover def _alpn_selection(self, _connection: SSL.Connection, alpn_protos: List[bytes]) -> bytes: """Callback to select alpn protocol.""" diff --git a/acme/tests/challenges_test.py b/acme/tests/challenges_test.py index d7815a6c3..36cd1e376 100644 --- a/acme/tests/challenges_test.py +++ b/acme/tests/challenges_test.py @@ -328,12 +328,12 @@ class TLSALPN01ResponseTest(unittest.TestCase): mock_gethostbyname.assert_called_once_with('foo.com') mock_probe_sni.assert_called_once_with( host=b'127.0.0.1', port=self.response.PORT, name=b'foo.com', - alpn_protocols=['acme-tls/1']) + alpn_protocols=[b'acme-tls/1']) self.response.probe_cert('foo.com', host='8.8.8.8') mock_probe_sni.assert_called_with( host=b'8.8.8.8', port=mock.ANY, name=b'foo.com', - alpn_protocols=['acme-tls/1']) + alpn_protocols=[b'acme-tls/1']) @mock.patch('acme.challenges.TLSALPN01Response.probe_cert') def test_simple_verify_false_on_probe_error(self, mock_probe_cert): diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py b/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py index 3650f64f0..3563b30af 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py @@ -125,7 +125,7 @@ def assert_equals_world_read_permissions(file1: str, file2: str) -> None: mode_file1 = os.stat(file1).st_mode & 0o004 mode_file2 = os.stat(file2).st_mode & 0o004 else: - everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID) + everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID) # pylint: disable=used-before-assignment security1 = win32security.GetFileSecurity(file1, win32security.DACL_SECURITY_INFORMATION) dacl1 = security1.GetSecurityDescriptorDacl() @@ -135,7 +135,7 @@ def assert_equals_world_read_permissions(file1: str, file2: str) -> None: 'TrusteeType': win32security.TRUSTEE_IS_USER, 'Identifier': everybody, }) - mode_file1 = mode_file1 & ntsecuritycon.FILE_GENERIC_READ + mode_file1 = mode_file1 & ntsecuritycon.FILE_GENERIC_READ # pylint: disable=used-before-assignment security2 = win32security.GetFileSecurity(file2, win32security.DACL_SECURITY_INFORMATION) dacl2 = security2.GetSecurityDescriptorDacl() diff --git a/certbot-ci/certbot_integration_tests/utils/acme_server.py b/certbot-ci/certbot_integration_tests/utils/acme_server.py index 9e56e3036..8994d0ef0 100755 --- a/certbot-ci/certbot_integration_tests/utils/acme_server.py +++ b/certbot-ci/certbot_integration_tests/utils/acme_server.py @@ -133,18 +133,9 @@ class ACMEServer: acme_xdist['directory_url'] = BOULDER_V2_DIRECTORY_URL acme_xdist['challtestsrv_url'] = BOULDER_V2_CHALLTESTSRV_URL - acme_xdist['http_port'] = { - node: port for (node, port) in # pylint: disable=unnecessary-comprehension - zip(nodes, range(5200, 5200 + len(nodes))) - } - acme_xdist['https_port'] = { - node: port for (node, port) in # pylint: disable=unnecessary-comprehension - zip(nodes, range(5100, 5100 + len(nodes))) - } - acme_xdist['other_port'] = { - node: port for (node, port) in # pylint: disable=unnecessary-comprehension - zip(nodes, range(5300, 5300 + len(nodes))) - } + acme_xdist['http_port'] = dict(zip(nodes, range(5200, 5200 + len(nodes)))) + acme_xdist['https_port'] = dict(zip(nodes, range(5100, 5100 + len(nodes)))) + acme_xdist['other_port'] = dict(zip(nodes, range(5300, 5300 + len(nodes)))) self.acme_xdist = acme_xdist diff --git a/certbot-dns-route53/certbot_dns_route53/_internal/dns_route53.py b/certbot-dns-route53/certbot_dns_route53/_internal/dns_route53.py index cbe22271f..fc2fe5821 100644 --- a/certbot-dns-route53/certbot_dns_route53/_internal/dns_route53.py +++ b/certbot-dns-route53/certbot_dns_route53/_internal/dns_route53.py @@ -38,7 +38,8 @@ class Authenticator(dns_common.DNSAuthenticator): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.r53 = boto3.client("route53") - self._resource_records: DefaultDict[str, List[Dict[str, str]]] = collections.defaultdict(list) + self._resource_records: DefaultDict[str, List[Dict[str, str]]] = \ + collections.defaultdict(list) def more_info(self) -> str: return "Solve a DNS01 challenge using AWS Route53" diff --git a/certbot/certbot/_internal/lock.py b/certbot/certbot/_internal/lock.py index 95c44856f..960be4346 100644 --- a/certbot/certbot/_internal/lock.py +++ b/certbot/certbot/_internal/lock.py @@ -194,7 +194,7 @@ class _WindowsLockMechanism(_BaseLockMechanism): low level APIs, and Python does not do it. As of Python 3.7 and below, Python developers state that deleting a file opened by a process from another process is not possible with os.open and io.open. - Consequently, mscvrt.locking is sufficient to obtain an effective lock, and the race + Consequently, msvcrt.locking is sufficient to obtain an effective lock, and the race condition encountered on Linux is not possible on Windows, leading to a simpler workflow. """ def acquire(self) -> None: @@ -209,7 +209,7 @@ class _WindowsLockMechanism(_BaseLockMechanism): # This "type: ignore" is currently needed because msvcrt methods # are only defined on Windows. See # https://github.com/python/typeshed/blob/16ae4c61201cd8b96b8b22cdfb2ab9e89ba5bcf2/stdlib/msvcrt.pyi. - msvcrt.locking(fd, msvcrt.LK_NBLCK, 1) # type: ignore + msvcrt.locking(fd, msvcrt.LK_NBLCK, 1) # type: ignore # pylint: disable=used-before-assignment except (IOError, OSError) as err: if fd: os.close(fd) @@ -229,7 +229,7 @@ class _WindowsLockMechanism(_BaseLockMechanism): # This "type: ignore" is currently needed because msvcrt methods # are only defined on Windows. See # https://github.com/python/typeshed/blob/16ae4c61201cd8b96b8b22cdfb2ab9e89ba5bcf2/stdlib/msvcrt.pyi. - msvcrt.locking(self._fd, msvcrt.LK_UNLCK, 1) # type: ignore + msvcrt.locking(self._fd, msvcrt.LK_UNLCK, 1) # type: ignore # pylint: disable=used-before-assignment os.close(self._fd) try: diff --git a/certbot/certbot/achallenges.py b/certbot/certbot/achallenges.py index 73666825c..081fa1c46 100644 --- a/certbot/certbot/achallenges.py +++ b/certbot/certbot/achallenges.py @@ -47,7 +47,7 @@ class AnnotatedChallenge(jose.ImmutableMap): class KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge): """Client annotated `KeyAuthorizationChallenge` challenge.""" - __slots__ = ('challb', 'domain', 'account_key') + __slots__ = ('challb', 'domain', 'account_key') # pylint: disable=redefined-slots-in-subclass def response_and_validation(self, *args: Any, **kwargs: Any) -> Any: """Generate response and validation.""" @@ -57,5 +57,5 @@ class KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge): class DNS(AnnotatedChallenge): """Client annotated "dns" ACME challenge.""" - __slots__ = ('challb', 'domain') + __slots__ = ('challb', 'domain') # pylint: disable=redefined-slots-in-subclass acme_type = challenges.DNS diff --git a/certbot/certbot/plugins/dns_test_common.py b/certbot/certbot/plugins/dns_test_common.py index ae8a69b79..a2ab84dcb 100644 --- a/certbot/certbot/plugins/dns_test_common.py +++ b/certbot/certbot/plugins/dns_test_common.py @@ -38,14 +38,12 @@ class _AuthenticatorCallableTestCase(Protocol): See https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertTrue """ - ... def assertEqual(self, *unused_args: Any) -> None: """ See https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertEqual """ - ... class BaseAuthenticatorTest: diff --git a/certbot/certbot/plugins/dns_test_common_lexicon.py b/certbot/certbot/plugins/dns_test_common_lexicon.py index 7d844d133..01f4c6d61 100644 --- a/certbot/certbot/plugins/dns_test_common_lexicon.py +++ b/certbot/certbot/plugins/dns_test_common_lexicon.py @@ -57,7 +57,6 @@ class _LexiconAwareTestCase(Protocol): See https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaises """ - ... # These classes are intended to be subclassed/mixed in, so not all members are defined. diff --git a/certbot/certbot/util.py b/certbot/certbot/util.py index 242dfef5a..9a6c5de78 100644 --- a/certbot/certbot/util.py +++ b/certbot/certbot/util.py @@ -34,7 +34,7 @@ if _USE_DISTRO: import distro if TYPE_CHECKING: - import distutils.version + import distutils.version # pylint: disable=deprecated-module logger = logging.getLogger(__name__) @@ -624,7 +624,7 @@ def get_strict_version(normalized: str) -> "distutils.version.StrictVersion": "removed in a future release.", DeprecationWarning) with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) - import distutils.version + import distutils.version # pylint: disable=deprecated-module # strict version ending with "a" and a number designates a pre-release return distutils.version.StrictVersion(normalized.replace(".dev", "a")) diff --git a/certbot/docs/conf.py b/certbot/docs/conf.py index 08bb3705a..de9e287fc 100644 --- a/certbot/docs/conf.py +++ b/certbot/docs/conf.py @@ -84,7 +84,7 @@ release = meta['version'] # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/tools/pinning/current/pyproject.toml b/tools/pinning/current/pyproject.toml index b5959bc20..95b236c10 100644 --- a/tools/pinning/current/pyproject.toml +++ b/tools/pinning/current/pyproject.toml @@ -62,7 +62,14 @@ mock = "*" # https://github.com/python-poetry/poetry/issues/1584. This version is required # here in addition to certbot/setup.py because otherwise the pre-release # version of poetry will not be installed. -poetry = ">=1.2.0a1" +# +# Additionally, newer versions of poetry/poetry-core include package extras +# (e.g. "docker[ssh]") in its `poetry export` output which is valid for +# requirements files, but not constraints files. We are currently using that +# output as constraints so let's also pin back poetry and poetry-core until we +# have a better workaround. +poetry = "1.2.0a2" +poetry-core = "1.1.0a7" # setuptools-rust is a build dependency of cryptography, and since we don't have # a great way of pinning build dependencies, we simply list it here to ensure a # working version. Note: if build dependencies of setuptools-rust break at some diff --git a/tools/requirements.txt b/tools/requirements.txt index d57f3a600..0bc823444 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -7,190 +7,191 @@ # for more info. alabaster==0.7.12; python_version >= "3.7" apacheconfig==0.3.2; python_version >= "3.7" -appdirs==1.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.4.0" -appnope==0.1.2; python_version >= "3.7" and sys_platform == "darwin" -astroid==2.9.3; python_version >= "3.7" and python_full_version >= "3.6.2" -atomicwrites==1.4.0; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.4.0" -attrs==21.4.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -awscli==1.22.75; python_version >= "3.6" +appdirs==1.4.4; python_version >= "3.7" and python_version < "4.0" +appnope==0.1.3; python_version >= "3.7" and sys_platform == "darwin" +astroid==2.11.7; python_version >= "3.7" +atomicwrites==1.4.1; sys_platform == "win32" and python_version >= "3.7" +attrs==22.1.0; python_version >= "3.7" +awscli==1.25.40 +awscli==1.25.40; python_version >= "3.7" azure-devops==6.0.0b4; python_version >= "3.7" -babel==2.9.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.4.0" +babel==2.10.3; python_version >= "3.7" backcall==0.2.0; python_version >= "3.7" -bcrypt==3.2.0; python_version >= "3.7" -beautifulsoup4==4.10.0; python_full_version > "3.0.0" and python_version >= "3.7" or python_version >= "3.7" and python_version < "4.0" and python_full_version > "3.0.0" -bleach==4.1.0; python_version >= "3.7" -boto3==1.21.20; python_version >= "3.7" -botocore==1.24.20; python_version >= "3.7" -cachecontrol==0.12.10; python_version >= "3.7" and python_version < "4.0" -cached-property==1.5.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" -cachetools==5.0.0; python_version >= "3.7" and python_version < "4.0" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7") -cachy==0.3.0; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.4.0" -certifi==2021.10.8; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7" or python_version >= "3.7" -cffi==1.15.0; python_version >= "3.7" or python_version >= "3.7" -charset-normalizer==2.0.12; python_full_version >= "3.6.0" and python_version >= "3.7" -cleo==1.0.0a4; python_version >= "3.7" and python_version < "4.0" -cloudflare==2.8.15; python_version >= "3.7" -colorama==0.4.3; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" or sys_platform == "win32" and python_full_version >= "3.6.2" and python_version >= "3.7" or python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or python_full_version >= "3.5.0" and python_version >= "3.7" and sys_platform == "win32" or python_version >= "3.7" and python_full_version < "3.0.0" and platform_system == "Windows" or python_version >= "3.7" and python_full_version >= "3.5.0" and platform_system == "Windows" -configargparse==1.5.3; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" +bcrypt==3.2.2; python_version >= "3.7" +beautifulsoup4==4.11.1; python_version >= "3.7" +bleach==5.0.1; python_version >= "3.7" +boto3==1.24.40; python_version >= "3.7" +botocore==1.27.40; python_version >= "3.7" +cachecontrol==0.12.11; python_version >= "3.7" and python_version < "4.0" +cached-property==1.5.2; python_version >= "3.7" +cachetools==5.2.0; python_version >= "3.7" and python_version < "4.0" +cachy==0.3.0; python_version >= "3.7" and python_version < "4.0" +certifi==2022.6.15; python_version >= "3.7" and python_version < "4" or python_version >= "3.7" +cffi==1.15.1; python_version >= "3.7" +charset-normalizer==2.1.0; python_version >= "3.7" and python_version < "4" +cleo==1.0.0a5; python_version >= "3.7" and python_version < "4.0" +cloudflare==2.9.11; python_version >= "3.7" +colorama==0.4.4; python_version >= "3.7" +configargparse==1.5.3; python_version >= "3.7" configobj==5.0.6; python_version >= "3.7" -coverage==6.3.2; python_version >= "3.7" or python_version >= "3.7" +coverage==6.4.2; python_version >= "3.7" crashtest==0.3.1; python_version >= "3.7" and python_version < "4.0" -cryptography==36.0.2; python_version >= "3.7" and python_version < "4.0" or python_version >= "3.7" or python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux" -cython==0.29.28; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") +cryptography==37.0.4 +cryptography==37.0.4; python_version >= "3.7" +cython==0.29.31 decorator==5.1.1; python_version >= "3.7" -deprecated==1.2.13; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.4.0" -distlib==0.3.4; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.4.0" or python_version >= "3.7" -distro==1.7.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" or python_version >= "3.7" -dns-lexicon==3.9.4; python_version >= "3.7" and python_version < "4.0" +dill==0.3.5.1; python_version >= "3.7" +distlib==0.3.5; python_version >= "3.7" +distro==1.7.0; python_version >= "3.7" +dns-lexicon==3.11.1; python_version >= "3.7" and python_version < "4.0" dnspython==2.2.1; python_version >= "3.7" and python_version < "4.0" -docker-compose==1.26.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" -docker==4.2.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -dockerpty==0.4.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" -docopt==0.6.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" -docutils==0.15.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7" or python_version >= "3.7" and python_full_version >= "3.4.0" +docker-compose==1.26.2; python_version >= "3.7" +docker==4.2.2; python_version >= "3.7" +dockerpty==0.4.1; python_version >= "3.7" +docopt==0.6.2; python_version >= "3.7" +docutils==0.16; python_version >= "3.7" entrypoints==0.3; python_version >= "3.7" and python_version < "4.0" -execnet==1.9.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -fabric==2.6.0; python_version >= "3.7" -filelock==3.6.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.5.0" or python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.4.0" or python_version >= "3.7" and python_version < "4.0" -google-api-core==2.7.1; python_version >= "3.7" -google-api-python-client==2.41.0; python_version >= "3.7" +execnet==1.9.0; python_version >= "3.7" +fabric==2.7.1; python_version >= "3.7" +filelock==3.7.1; python_version >= "3.7" or python_version >= "3.7" and python_version < "4.0" +google-api-core==2.8.2; python_version >= "3.7" +google-api-python-client==2.55.0; python_version >= "3.7" google-auth-httplib2==0.1.0; python_version >= "3.7" -google-auth==2.6.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7" -googleapis-common-protos==1.55.0; python_version >= "3.7" -html5lib==1.1; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.5.0" -httplib2==0.20.4; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" -idna==3.3; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7" or python_version >= "3.7" and python_version < "4.0" -imagesize==1.3.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.4.0" -importlib-metadata==1.7.0; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "3.8" or python_version >= "3.7" and python_version < "3.8" and python_full_version >= "3.5.0" +google-auth==2.9.1; python_version >= "3.7" +googleapis-common-protos==1.56.4; python_version >= "3.7" +html5lib==1.1; python_version >= "3.7" and python_version < "4.0" +httplib2==0.20.4; python_version >= "3.7" +idna==3.3; python_version >= "3.7" and python_version < "4" or python_version >= "3.7" and python_version < "4.0" +imagesize==1.4.1; python_version >= "3.7" +importlib-metadata==1.7.0; python_version >= "3.7" and python_version < "3.8" iniconfig==1.1.1; python_version >= "3.7" -invoke==1.6.0; python_version >= "3.7" +invoke==1.7.1; python_version >= "3.7" ipdb==0.13.9; python_version >= "3.7" -ipython==7.32.0; python_version >= "3.7" +ipython==7.34.0; python_version >= "3.7" isodate==0.6.1; python_version >= "3.7" -isort==5.10.1; python_full_version >= "3.6.2" and python_version < "4.0" and python_version >= "3.7" +isort==5.10.1; python_version >= "3.7" and python_version < "4.0" jedi==0.18.1; python_version >= "3.7" -jeepney==0.7.1; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux" -jinja2==3.0.3; python_version >= "3.7" or python_version >= "3.7" -jmespath==0.10.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7" +jeepney==0.8.0; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux" +jinja2==3.1.2; python_version >= "3.7" +jmespath==1.0.1; python_version >= "3.7" josepy==1.13.0; python_version >= "3.7" -jsonlines==3.0.0; python_version >= "3.7" -jsonpickle==2.1.0; python_version >= "3.7" -jsonschema==3.2.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" +jsonlines==3.1.0; python_version >= "3.7" +jsonpickle==2.2.0; python_version >= "3.7" +jsonschema==3.2.0; python_version >= "3.7" keyring==22.3.0; python_version >= "3.7" and python_version < "4.0" or python_version >= "3.7" -lazy-object-proxy==1.7.1; python_version >= "3.7" and python_full_version >= "3.6.2" +lazy-object-proxy==1.7.1; python_version >= "3.7" lockfile==0.12.2 markupsafe==2.1.1; python_version >= "3.7" matplotlib-inline==0.1.3; python_version >= "3.7" -mccabe==0.6.1; python_version >= "3.7" and python_full_version >= "3.6.2" -mock==4.0.3; python_version >= "3.6" -msgpack==1.0.3; python_version >= "3.7" and python_version < "4.0" +mccabe==0.7.0; python_version >= "3.7" +mock==4.0.3 +msgpack==1.0.4; python_version >= "3.7" and python_version < "4.0" msrest==0.6.21; python_version >= "3.7" mypy-extensions==0.4.3; python_version >= "3.7" -mypy==0.941; python_version >= "3.7" +mypy==0.971; python_version >= "3.7" oauth2client==4.1.3; python_version >= "3.7" -oauthlib==3.2.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.4.0" -packaging==20.9; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.4.0" or python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" or python_version >= "3.7" and python_full_version >= "3.5.0" -paramiko==2.10.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" or python_version >= "3.7" +oauthlib==3.2.0; python_version >= "3.7" +packaging==20.9; python_version >= "3.7" +paramiko==2.11.0; python_version >= "3.7" parsedatetime==2.6; python_version >= "3.7" parso==0.8.3; python_version >= "3.7" pathlib2==2.3.7.post1; python_version >= "3.7" pexpect==4.8.0; python_version >= "3.7" and python_version < "4.0" or python_version >= "3.7" and sys_platform != "win32" pickleshare==0.7.5; python_version >= "3.7" -pip==22.0.4; python_version >= "3.7" -pkginfo==1.8.2; python_version >= "3.7" and python_version < "4.0" or python_version >= "3.7" -platformdirs==2.5.1; python_version >= "3.7" and python_full_version >= "3.6.2" -pluggy==1.0.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.5.0" or python_version >= "3.7" +pip==22.2.1; python_version >= "3.7" +pkginfo==1.8.3; python_version >= "3.7" and python_version < "4.0" or python_version >= "3.7" +platformdirs==2.5.2; python_version >= "3.7" +pluggy==1.0.0; python_version >= "3.7" ply==3.11; python_version >= "3.7" -poetry-core==1.1.0a7; python_version >= "3.7" and python_version < "4.0" -poetry==1.2.0a2; python_version >= "3.6" and python_version < "4.0" -prompt-toolkit==3.0.28; python_version >= "3.7" and python_full_version >= "3.6.2" -protobuf==3.19.4; python_version >= "3.7" +poetry-core==1.1.0a7 +poetry==1.2.0a2 +prompt-toolkit==3.0.30; python_version >= "3.7" +protobuf==4.21.4; python_version >= "3.7" ptyprocess==0.7.0; python_version >= "3.7" and python_version < "4.0" -py==1.11.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -pyasn1-modules==0.2.8; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7" or python_version >= "3.7" -pyasn1==0.4.8; python_version >= "3.7" and python_version < "4" or python_version >= "3.7" -pycparser==2.21; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" -pygithub==1.55; python_version >= "3.7" -pygments==2.11.2; python_version >= "3.7" -pyjwt==2.3.0; python_version >= "3.7" +py==1.11.0; python_version >= "3.7" +pyasn1-modules==0.2.8; python_version >= "3.7" +pyasn1==0.4.8; python_version >= "3.7" +pycparser==2.21; python_version >= "3.7" +pygments==2.12.0; python_version >= "3.7" pylev==1.4.0; python_version >= "3.7" and python_version < "4.0" -pylint==2.12.2; python_full_version >= "3.6.2" -pynacl==1.5.0; python_version >= "3.7" or python_version >= "3.7" +pylint==2.13.9 +pylint==2.13.9; python_version >= "3.7" +pynacl==1.5.0; python_version >= "3.7" pynsist==2.7; python_version >= "3.7" pyopenssl==22.0.0; python_version >= "3.7" -pyparsing==3.0.7; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" or python_version >= "3.7" or python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.4.0" -pypiwin32==223; sys_platform == "win32" and python_version >= "3.6" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7") +pyparsing==3.0.9; python_version >= "3.7" +pypiwin32==223; sys_platform == "win32" and python_version >= "3.7" pyrfc3339==1.1; python_version >= "3.7" -pyrsistent==0.18.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" -pytest-cov==3.0.0; python_version >= "3.7" or python_version >= "3.7" +pyrsistent==0.18.1; python_version >= "3.7" +pytest-cov==3.0.0; python_version >= "3.7" pytest-forked==1.4.0; python_version >= "3.7" -pytest-xdist==2.5.0; python_version >= "3.7" or python_version >= "3.7" -pytest==7.1.0; python_version >= "3.7" or python_version >= "3.7" +pytest-xdist==2.5.0; python_version >= "3.7" +pytest==7.1.2; python_version >= "3.7" python-augeas==1.1.0; python_version >= "3.7" -python-dateutil==2.8.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7" +python-dateutil==2.8.2; python_version >= "3.7" python-digitalocean==1.17.0; python_version >= "3.7" -python-dotenv==0.19.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" -pytz==2021.3; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.4.0" or python_version >= "3.7" +python-dotenv==0.20.0; python_version >= "3.7" +pytz==2022.1; python_version >= "3.7" pywin32-ctypes==0.2.0; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" -pywin32==303; sys_platform == "win32" and python_version >= "3.7" or sys_platform == "win32" and python_version >= "3.6" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7") -pyyaml==5.4.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7" or python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.6.0" -readme-renderer==34.0; python_version >= "3.7" +pywin32==304; sys_platform == "win32" and python_version >= "3.7" +pyyaml==5.4.1; python_version >= "3.7" +readme-renderer==35.0; python_version >= "3.7" requests-download==0.1.2; python_version >= "3.7" requests-file==1.5.1; python_version >= "3.7" and python_version < "4.0" -requests-oauthlib==1.3.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.4.0" -requests-toolbelt==0.9.1; python_version >= "3.7" and python_version < "4.0" or python_version >= "3.7" or python_version >= "3.7" -requests==2.27.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7" or python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.6.0" +requests-oauthlib==1.3.1; python_version >= "3.7" +requests-toolbelt==0.9.1; python_version >= "3.7" +requests==2.28.1; python_version >= "3.7" and python_version < "4" rfc3986==2.0.0; python_version >= "3.7" -rsa==4.7.2; python_version >= "3.7" and python_version < "4" or python_version >= "3.5" and python_version < "4" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7") -s3transfer==0.5.2; python_version >= "3.7" -secretstorage==3.3.1; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux" -semantic-version==2.9.0; python_version >= "3.6" -setuptools-rust==1.1.2; python_version >= "3.6" -setuptools==60.9.3; python_version >= "3.7" or python_version >= "3.7" or python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" or python_version >= "3.7" and python_full_version >= "3.6.2" or python_full_version >= "3.4.0" and python_version >= "3.7" +rsa==4.7.2; python_version >= "3.7" and python_version < "4" +s3transfer==0.6.0; python_version >= "3.7" +secretstorage==3.3.2; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux" +semantic-version==2.10.0; python_version >= "3.7" +setuptools-rust==1.4.1 +setuptools==63.2.0; python_version >= "3.7" shellingham==1.4.0; python_version >= "3.7" and python_version < "4.0" -six==1.16.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" or python_full_version >= "3.3.0" and python_version >= "3.7" or python_version >= "3.7" and python_full_version >= "3.5.0" or python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.5.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.4.0" or python_full_version >= "3.6.0" and python_version >= "3.7" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.3.0" +six==1.16.0; python_version >= "3.7" snowballstemmer==2.2.0; python_version >= "3.7" -soupsieve==2.3.1; python_full_version > "3.0.0" and python_version >= "3.7" -sphinx-rtd-theme==1.0.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.4.0" -sphinx==4.3.2; python_version >= "3.7" or python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.4.0" +soupsieve==2.3.2.post1; python_version >= "3.7" +sphinx-rtd-theme==1.0.0; python_version >= "3.7" +sphinx==4.3.2; python_version >= "3.7" sphinxcontrib-applehelp==1.0.2; python_version >= "3.7" sphinxcontrib-devhelp==1.0.2; python_version >= "3.7" sphinxcontrib-htmlhelp==2.0.0; python_version >= "3.7" sphinxcontrib-jsmath==1.0.1; python_version >= "3.7" sphinxcontrib-qthelp==1.0.3; python_version >= "3.7" sphinxcontrib-serializinghtml==1.1.5; python_version >= "3.7" -texttable==1.6.4; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" -tldextract==3.2.0; python_version >= "3.7" and python_version < "4.0" -toml==0.10.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.3.0" or python_version >= "3.7" and python_full_version >= "3.6.2" or python_version >= "3.7" and python_full_version >= "3.5.0" -tomli==2.0.1; python_version >= "3.7" or python_version >= "3.7" -tomlkit==0.10.0; python_version >= "3.7" and python_version < "4.0" -tox==3.24.5; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.5.0" -tqdm==4.63.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.4.0" -traitlets==5.1.1; python_version >= "3.7" +texttable==1.6.4; python_version >= "3.7" +tldextract==3.3.1; python_version >= "3.7" and python_version < "4.0" +toml==0.10.2; python_version >= "3.7" +tomli==2.0.1; python_version < "3.11" and python_version >= "3.7" or python_full_version <= "3.11.0a6" and python_version >= "3.7" or python_version >= "3.7" +tomlkit==0.11.1; python_version >= "3.7" and python_version < "4.0" +tox==3.25.1; python_version >= "3.7" +tqdm==4.64.0; python_version >= "3.7" +traitlets==5.3.0; python_version >= "3.7" twine==3.3.0; python_version >= "3.7" -typed-ast==1.5.2; python_version >= "3.7" and python_version < "3.8" or implementation_name == "cpython" and python_version < "3.8" and python_version >= "3.7" and python_full_version >= "3.6.2" -types-cryptography==3.3.18; python_version >= "3.7" -types-mock==4.0.11; python_version >= "3.7" -types-pyopenssl==22.0.0; python_version >= "3.7" +typed-ast==1.5.4; python_version >= "3.7" and python_version < "3.8" or implementation_name == "cpython" and python_version < "3.8" and python_version >= "3.7" +types-cryptography==3.3.21; python_version >= "3.7" +types-mock==4.0.15; python_version >= "3.7" +types-pyopenssl==22.0.9; python_version >= "3.7" types-pyrfc3339==1.1.1; python_version >= "3.7" -types-python-dateutil==2.8.9; python_version >= "3.7" -types-pytz==2021.3.5; python_version >= "3.7" -types-requests==2.27.12; python_version >= "3.7" -types-setuptools==57.4.10; python_version >= "3.7" -types-six==1.16.12; python_version >= "3.7" -types-urllib3==1.26.11; python_version >= "3.7" -typing-extensions==4.1.1; python_version >= "3.7" or python_version >= "3.6" or python_version < "3.10" and python_full_version >= "3.6.2" and python_version >= "3.7" or python_version < "3.8" and python_version >= "3.7" +types-python-dateutil==2.8.19; python_version >= "3.7" +types-pytz==2022.1.2; python_version >= "3.7" +types-requests==2.28.5; python_version >= "3.7" +types-setuptools==63.2.2; python_version >= "3.7" +types-six==1.16.18; python_version >= "3.7" +types-urllib3==1.26.17; python_version >= "3.7" +typing-extensions==4.3.0; python_version >= "3.7" or python_version < "3.10" and python_version >= "3.7" or python_version < "3.8" and python_version >= "3.7" uritemplate==4.1.1; python_version >= "3.7" -urllib3==1.26.8; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.7" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.7" -virtualenv==20.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.4.0" or python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.5.0" -wcwidth==0.2.5; python_version >= "3.7" and python_full_version >= "3.6.2" -webencodings==0.5.1; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.5.0" or python_version >= "3.7" -websocket-client==0.59.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" or python_full_version >= "3.5.0" and python_version >= "3.7" -wheel==0.37.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.5.0" -wrapt==1.13.3; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.5.0" or python_version >= "3.7" and python_full_version >= "3.6.2" +urllib3==1.26.11; python_version >= "3.7" and python_version < "4" +virtualenv==20.4.4; python_version >= "3.7" and python_version < "4.0" or python_version >= "3.7" +wcwidth==0.2.5; python_version >= "3.7" +webencodings==0.5.1; python_version >= "3.7" and python_version < "4.0" or python_version >= "3.7" +websocket-client==0.59.0; python_version >= "3.7" +wheel==0.37.1; python_version >= "3.7" +wrapt==1.14.1; python_version >= "3.7" yarg==0.1.9; python_version >= "3.7" -zipp==3.7.0; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "3.8" or python_version >= "3.7" and python_version < "3.8" and python_full_version >= "3.5.0" -zope.component==5.0.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -zope.event==4.5.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -zope.hookable==5.1.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -zope.interface==5.4.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" +zipp==3.8.1; python_version >= "3.7" and python_version < "3.8" +zope.component==5.0.1; python_version >= "3.7" +zope.event==4.5.0; python_version >= "3.7" +zope.hookable==5.1.0; python_version >= "3.7" +zope.interface==5.4.0; python_version >= "3.7" From b73f3e2b16ce83836e4dbf3e7cfdc7acd2b318a2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 28 Jul 2022 19:58:47 -0700 Subject: [PATCH 06/29] pin back pylint (#9368) --- tools/oldest_constraints.txt | 43 ++++++++++++++-------------- tools/pinning/current/pyproject.toml | 12 ++++---- tools/pinning/oldest/pyproject.toml | 12 ++++---- tools/requirements.txt | 1 - 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/tools/oldest_constraints.txt b/tools/oldest_constraints.txt index 90ec56a34..16dfbb758 100644 --- a/tools/oldest_constraints.txt +++ b/tools/oldest_constraints.txt @@ -2,9 +2,9 @@ # that script. apacheconfig==0.3.2 asn1crypto==0.24.0 -astroid==2.11.6; python_version >= "3.7" -atomicwrites==1.4.0; sys_platform == "win32" and python_version >= "3.7" -attrs==21.4.0; python_version >= "3.7" +astroid==2.11.7; python_version >= "3.7" +atomicwrites==1.4.1; sys_platform == "win32" and python_version >= "3.7" +attrs==22.1.0; python_version >= "3.7" bcrypt==3.2.2; python_version >= "3.7" boto3==1.15.15 botocore==1.18.15 @@ -16,11 +16,11 @@ cloudflare==1.5.1 colorama==0.4.5; sys_platform == "win32" and python_version >= "3.7" configargparse==0.10.0 configobj==5.0.6 -coverage==6.4.1; python_version >= "3.7" +coverage==6.4.2; python_version >= "3.7" cryptography==3.2.1 -cython==0.29.30 +cython==0.29.31 dill==0.3.5.1; python_version >= "3.7" -distlib==0.3.4; python_version >= "3.7" +distlib==0.3.5; python_version >= "3.7" distro==1.0.1 dns-lexicon==3.2.1 dnspython==1.15.0 @@ -35,7 +35,7 @@ future==0.18.2; python_version >= "3.7" google-api-python-client==1.5.5 httplib2==0.9.2 idna==2.6 -importlib-metadata==4.11.4; python_version < "3.8" and python_version >= "3.7" +importlib-metadata==4.12.0; python_version < "3.8" and python_version >= "3.7" iniconfig==1.1.1; python_version >= "3.7" ipaddress==1.0.16 isort==5.10.1; python_version >= "3.7" and python_version < "4.0" @@ -47,14 +47,14 @@ logger==1.4; python_version >= "3.7" mccabe==0.7.0; python_version >= "3.7" mock==1.0.1 mypy-extensions==0.4.3; python_version >= "3.7" -mypy==0.961; python_version >= "3.7" +mypy==0.971; python_version >= "3.7" ndg-httpsclient==0.3.2 oauth2client==4.0.0 packaging==21.3; python_version >= "3.7" paramiko==2.11.0; python_version >= "3.7" parsedatetime==2.4 pbr==1.8.0 -pip==22.1.2; python_version >= "3.7" +pip==22.2.1; python_version >= "3.7" platformdirs==2.5.2; python_version >= "3.7" pluggy==1.0.0; python_version >= "3.7" ply==3.4 @@ -63,7 +63,6 @@ pyasn1-modules==0.0.10; python_version >= "3.7" pyasn1==0.1.9 pycparser==2.14 pylint==2.13.9 -pylint==2.13.9; python_version >= "3.7" pynacl==1.5.0; python_version >= "3.7" pyopenssl==17.5.0 pyparsing==2.2.1 @@ -83,34 +82,34 @@ pyyaml==5.4.1; python_version >= "3.7" requests-file==1.5.1; python_version >= "3.7" requests-toolbelt==0.9.1; python_version >= "3.7" requests==2.20.0 -rsa==4.8; python_version >= "3.7" and python_version < "4" +rsa==4.9; python_version >= "3.7" and python_version < "4" s3transfer==0.3.7; python_version >= "3.7" setuptools==41.6.0 six==1.11.0 texttable==1.6.4; python_version >= "3.7" -tldextract==3.3.0; python_version >= "3.7" +tldextract==3.3.1; python_version >= "3.7" tomli==2.0.1; python_version < "3.11" and python_version >= "3.7" or python_full_version <= "3.11.0a6" and python_version >= "3.7" or python_version >= "3.7" tox==1.9.2; python_version >= "3.7" typed-ast==1.5.4; python_version >= "3.7" and python_version < "3.8" or implementation_name == "cpython" and python_version < "3.8" and python_version >= "3.7" types-cryptography==3.3.21; python_version >= "3.7" types-mock==4.0.15; python_version >= "3.7" -types-pyopenssl==22.0.3; python_version >= "3.7" +types-pyopenssl==22.0.9; python_version >= "3.7" types-pyrfc3339==1.1.1; python_version >= "3.7" -types-python-dateutil==2.8.17; python_version >= "3.7" -types-pytz==2022.1.0; python_version >= "3.7" -types-requests==2.27.31; python_version >= "3.7" -types-setuptools==57.4.17; python_version >= "3.7" -types-six==1.16.16; python_version >= "3.7" -types-urllib3==1.26.15; python_version >= "3.7" -typing-extensions==4.2.0; python_version >= "3.7" or python_version < "3.10" and python_version >= "3.7" or python_version < "3.8" and python_version >= "3.7" +types-python-dateutil==2.8.19; python_version >= "3.7" +types-pytz==2022.1.2; python_version >= "3.7" +types-requests==2.28.5; python_version >= "3.7" +types-setuptools==63.2.2; python_version >= "3.7" +types-six==1.16.18; python_version >= "3.7" +types-urllib3==1.26.17; python_version >= "3.7" +typing-extensions==4.3.0; python_version >= "3.7" or python_version < "3.10" and python_version >= "3.7" or python_version < "3.8" and python_version >= "3.7" uritemplate==3.0.1; python_version >= "3.7" urllib3==1.24.2 -virtualenv==20.14.1; python_version >= "3.7" +virtualenv==20.16.2; python_version >= "3.7" websocket-client==0.59.0; python_version >= "3.7" wheel==0.33.6 wheel==0.33.6; python_version >= "3.7" wrapt==1.14.1; python_version >= "3.7" -zipp==3.8.0; python_version < "3.8" and python_version >= "3.7" +zipp==3.8.1; python_version < "3.8" and python_version >= "3.7" zope.component==4.1.0 zope.event==4.0.3 zope.hookable==4.0.4 diff --git a/tools/pinning/current/pyproject.toml b/tools/pinning/current/pyproject.toml index 95b236c10..99294c96e 100644 --- a/tools/pinning/current/pyproject.toml +++ b/tools/pinning/current/pyproject.toml @@ -76,11 +76,13 @@ poetry-core = "1.1.0a7" # point, it's probably worth enumerating and pinning them (and recursing to # THEIR build dependencies) as well. setuptools-rust = "*" -# A bad python_requires constraint in pylint 2.6.2 can sometimes crash poetry -# before it has finished resolving. No newer releases have the same issue. Remove -# this once we can upgrade to a release of Poetry containing this commit: -# https://github.com/python-poetry/poetry-core/commit/4e1f2ab582d1fef0033c0d3f35a3f2f2365a4bc9 -pylint = ">2.6.2" +# pylint often adds new checks that we need to conform our code to when +# upgrading our dependencies. To help control when this needs to be done, we +# pin pylint to a compatible version here. +# +# If this pinning is removed, we may still need to add a lower bound for the +# pylint version. See https://github.com/certbot/certbot/pull/9229. +pylint = "2.13.9" # Bug in poetry, where still installes yanked versions from pypi (source: https://github.com/python-poetry/poetry/issues/2453) # this version of cryptography introduced a security vulnrability. diff --git a/tools/pinning/oldest/pyproject.toml b/tools/pinning/oldest/pyproject.toml index 5585a39f4..3a49b12ed 100644 --- a/tools/pinning/oldest/pyproject.toml +++ b/tools/pinning/oldest/pyproject.toml @@ -104,11 +104,13 @@ cython = "*" # wheel 0.34.0 is buggy). wheel = "<0.34.0" -# A bad python_requires constraint in pylint 2.6.2 can sometimes crash poetry -# before it has finished resolving. No newer releases have the same issue. Remove -# this once we can upgrade to a release of Poetry containing this commit: -# https://github.com/python-poetry/poetry-core/commit/4e1f2ab582d1fef0033c0d3f35a3f2f2365a4bc9 -pylint = ">2.6.2" +# pylint often adds new checks that we need to conform our code to when +# upgrading our dependencies. To help control when this needs to be done, we +# pin pylint to a compatible version here. +# +# If this pinning is removed, we may still need to add a lower bound for the +# pylint version. See https://github.com/certbot/certbot/pull/9229. +pylint = "2.13.9" [tool.poetry.dev-dependencies] diff --git a/tools/requirements.txt b/tools/requirements.txt index 0bc823444..8dc4f0a93 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -115,7 +115,6 @@ pycparser==2.21; python_version >= "3.7" pygments==2.12.0; python_version >= "3.7" pylev==1.4.0; python_version >= "3.7" and python_version < "4.0" pylint==2.13.9 -pylint==2.13.9; python_version >= "3.7" pynacl==1.5.0; python_version >= "3.7" pynsist==2.7; python_version >= "3.7" pyopenssl==22.0.0; python_version >= "3.7" From 1b79c077a69a168e1d62bb8227db30551535a060 Mon Sep 17 00:00:00 2001 From: Gusmanov Timur Date: Sat, 30 Jul 2022 01:01:01 +0600 Subject: [PATCH 07/29] add dns-yandexcloud authentication plugin to third-party plugins (#9371) --- certbot/docs/using.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst index 38aaf4ff4..980457141 100644 --- a/certbot/docs/using.rst +++ b/certbot/docs/using.rst @@ -316,6 +316,7 @@ dns-lightsail_ Y N DNS Authentication using Amazon Lightsail DNS API dns-inwx_ Y Y DNS Authentication for INWX through the XML API dns-azure_ Y N DNS Authentication using Azure DNS dns-godaddy_ Y N DNS Authentication using Godaddy DNS +dns-yandexcloud_ Y N DNS Authentication using Yandex Cloud DNS njalla_ Y N DNS Authentication for njalla DuckDNS_ Y N DNS Authentication for DuckDNS Porkbun_ Y N DNS Authentication for Porkbun @@ -336,6 +337,7 @@ Infomaniak_ Y N DNS Authentication using Infomaniak Domains API .. _dns-inwx: https://github.com/oGGy990/certbot-dns-inwx/ .. _dns-azure: https://github.com/binkhq/certbot-dns-azure .. _dns-godaddy: https://github.com/miigotu/certbot-dns-godaddy +.. _dns-yandexcloud: https://github.com/PykupeJIbc/certbot-dns-yandexcloud .. _njalla: https://github.com/chaptergy/certbot-dns-njalla .. _DuckDNS: https://github.com/infinityofspace/certbot_dns_duckdns .. _Porkbun: https://github.com/infinityofspace/certbot_dns_porkbun From 2574a8dfb5fa07e734b15104bc80dd90d78306e8 Mon Sep 17 00:00:00 2001 From: alexzorin Date: Thu, 11 Aug 2022 04:01:11 +1000 Subject: [PATCH 08/29] remove all cloudxns-related code (#9361) --- certbot-dns-cloudxns/LICENSE.txt | 190 ------------------ certbot-dns-cloudxns/MANIFEST.in | 7 - certbot-dns-cloudxns/README.rst | 1 - .../certbot_dns_cloudxns/__init__.py | 90 --------- .../_internal/__init__.py | 1 - .../_internal/dns_cloudxns.py | 93 --------- .../certbot_dns_cloudxns/py.typed | 0 certbot-dns-cloudxns/docs/.gitignore | 1 - certbot-dns-cloudxns/docs/Makefile | 20 -- certbot-dns-cloudxns/docs/api.rst | 5 - certbot-dns-cloudxns/docs/conf.py | 181 ----------------- certbot-dns-cloudxns/docs/index.rst | 28 --- certbot-dns-cloudxns/docs/make.bat | 36 ---- .../readthedocs.org.requirements.txt | 15 -- certbot-dns-cloudxns/setup.py | 73 ------- .../tests/dns_cloudxns_test.py | 58 ------ .../certbot/_internal/cli/plugins_parsing.py | 4 - certbot/certbot/_internal/constants.py | 1 - certbot/certbot/_internal/plugins/disco.py | 1 - .../certbot/_internal/plugins/selection.py | 4 +- certbot/docs/packaging.rst | 1 - certbot/docs/using.rst | 1 - tools/_release.sh | 2 +- tools/docker/lib/common | 1 - tools/pinning/current/pyproject.toml | 1 - tools/pinning/oldest/pyproject.toml | 1 - tools/venv.py | 1 - tox.cover.py | 3 +- tox.ini | 4 +- 29 files changed, 5 insertions(+), 819 deletions(-) delete mode 100644 certbot-dns-cloudxns/LICENSE.txt delete mode 100644 certbot-dns-cloudxns/MANIFEST.in delete mode 100644 certbot-dns-cloudxns/README.rst delete mode 100644 certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py delete mode 100644 certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/__init__.py delete mode 100644 certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py delete mode 100644 certbot-dns-cloudxns/certbot_dns_cloudxns/py.typed delete mode 100644 certbot-dns-cloudxns/docs/.gitignore delete mode 100644 certbot-dns-cloudxns/docs/Makefile delete mode 100644 certbot-dns-cloudxns/docs/api.rst delete mode 100644 certbot-dns-cloudxns/docs/conf.py delete mode 100644 certbot-dns-cloudxns/docs/index.rst delete mode 100644 certbot-dns-cloudxns/docs/make.bat delete mode 100644 certbot-dns-cloudxns/readthedocs.org.requirements.txt delete mode 100644 certbot-dns-cloudxns/setup.py delete mode 100644 certbot-dns-cloudxns/tests/dns_cloudxns_test.py diff --git a/certbot-dns-cloudxns/LICENSE.txt b/certbot-dns-cloudxns/LICENSE.txt deleted file mode 100644 index 981c46c9f..000000000 --- a/certbot-dns-cloudxns/LICENSE.txt +++ /dev/null @@ -1,190 +0,0 @@ - Copyright 2015 Electronic Frontier Foundation and others - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/certbot-dns-cloudxns/MANIFEST.in b/certbot-dns-cloudxns/MANIFEST.in deleted file mode 100644 index 20810e7eb..000000000 --- a/certbot-dns-cloudxns/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -include LICENSE.txt -include README.rst -recursive-include docs * -recursive-include tests * -include certbot_dns_cloudxns/py.typed -global-exclude __pycache__ -global-exclude *.py[cod] diff --git a/certbot-dns-cloudxns/README.rst b/certbot-dns-cloudxns/README.rst deleted file mode 100644 index b127770df..000000000 --- a/certbot-dns-cloudxns/README.rst +++ /dev/null @@ -1 +0,0 @@ -CloudXNS DNS Authenticator plugin for Certbot diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py deleted file mode 100644 index 0d098445c..000000000 --- a/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -The `~certbot_dns_cloudxns.dns_cloudxns` plugin automates the process of -completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and -subsequently removing, TXT records using the CloudXNS API. - -.. note:: - The plugin is not installed by default. It can be installed by heading to - `certbot.eff.org `_, choosing your system and - selecting the Wildcard tab. - -Named Arguments ---------------- - -======================================== ===================================== -``--dns-cloudxns-credentials`` CloudXNS credentials_ INI file. - (Required) -``--dns-cloudxns-propagation-seconds`` The number of seconds to wait for DNS - to propagate before asking the ACME - server to verify the DNS record. - (Default: 30) -======================================== ===================================== - - -Credentials ------------ - -Use of this plugin requires a configuration file containing CloudXNS API -credentials, obtained from your CloudXNS -`API page `_. - -.. code-block:: ini - :name: credentials.ini - :caption: Example credentials file: - - # CloudXNS API credentials used by Certbot - dns_cloudxns_api_key = 1234567890abcdef1234567890abcdef - dns_cloudxns_secret_key = 1122334455667788 - -The path to this file can be provided interactively or using the -``--dns-cloudxns-credentials`` command-line argument. Certbot records the path -to this file for use during renewal, but does not store the file's contents. - -.. caution:: - You should protect these API credentials as you would the password to your - CloudXNS account. Users who can read this file can use these credentials to - issue arbitrary API calls on your behalf. Users who can cause Certbot to run - using these credentials can complete a ``dns-01`` challenge to acquire new - certificates or revoke existing certificates for associated domains, even if - those domains aren't being managed by this server. - -Certbot will emit a warning if it detects that the credentials file can be -accessed by other users on your system. The warning reads "Unsafe permissions -on credentials configuration file", followed by the path to the credentials -file. This warning will be emitted each time Certbot uses the credentials file, -including for renewal, and cannot be silenced except by addressing the issue -(e.g., by using a command like ``chmod 600`` to restrict access to the file). - - -Examples --------- - -.. code-block:: bash - :caption: To acquire a certificate for ``example.com`` - - certbot certonly \\ - --dns-cloudxns \\ - --dns-cloudxns-credentials ~/.secrets/certbot/cloudxns.ini \\ - -d example.com - -.. code-block:: bash - :caption: To acquire a single certificate for both ``example.com`` and - ``www.example.com`` - - certbot certonly \\ - --dns-cloudxns \\ - --dns-cloudxns-credentials ~/.secrets/certbot/cloudxns.ini \\ - -d example.com \\ - -d www.example.com - -.. code-block:: bash - :caption: To acquire a certificate for ``example.com``, waiting 60 seconds - for DNS propagation - - certbot certonly \\ - --dns-cloudxns \\ - --dns-cloudxns-credentials ~/.secrets/certbot/cloudxns.ini \\ - --dns-cloudxns-propagation-seconds 60 \\ - -d example.com - -""" diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/__init__.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/__init__.py deleted file mode 100644 index e2177417d..000000000 --- a/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Internal implementation of `~certbot_dns_cloudxns.dns_cloudxns` plugin.""" diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py deleted file mode 100644 index 15cdf8aa8..000000000 --- a/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py +++ /dev/null @@ -1,93 +0,0 @@ -"""DNS Authenticator for CloudXNS DNS.""" -import logging -from typing import Any -from typing import Callable -from typing import Optional - -from lexicon.providers import cloudxns -from requests import HTTPError - -from certbot import errors -from certbot.plugins import dns_common -from certbot.plugins import dns_common_lexicon -from certbot.plugins.dns_common import CredentialsConfiguration - -logger = logging.getLogger(__name__) - -ACCOUNT_URL = 'https://www.cloudxns.net/en/AccountManage/apimanage.html' - - -class Authenticator(dns_common.DNSAuthenticator): - """DNS Authenticator for CloudXNS DNS - - This Authenticator uses the CloudXNS DNS API to fulfill a dns-01 challenge. - """ - - description = 'Obtain certificates using a DNS TXT record (if you are using CloudXNS for DNS).' - ttl = 60 - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.credentials: Optional[CredentialsConfiguration] = None - - @classmethod - def add_parser_arguments(cls, add: Callable[..., None], - default_propagation_seconds: int = 30) -> None: - super().add_parser_arguments(add, default_propagation_seconds) - add('credentials', help='CloudXNS credentials INI file.') - - def more_info(self) -> str: - return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \ - 'the CloudXNS API.' - - def _setup_credentials(self) -> None: - self.credentials = self._configure_credentials( - 'credentials', - 'CloudXNS credentials INI file', - { - 'api-key': 'API key for CloudXNS account, obtained from {0}'.format(ACCOUNT_URL), - 'secret-key': 'Secret key for CloudXNS account, obtained from {0}' - .format(ACCOUNT_URL) - } - ) - - def _perform(self, domain: str, validation_name: str, validation: str) -> None: - self._get_cloudxns_client().add_txt_record(domain, validation_name, validation) - - def _cleanup(self, domain: str, validation_name: str, validation: str) -> None: - self._get_cloudxns_client().del_txt_record(domain, validation_name, validation) - - def _get_cloudxns_client(self) -> "_CloudXNSLexiconClient": - if not self.credentials: # pragma: no cover - raise errors.Error("Plugin has not been prepared.") - return _CloudXNSLexiconClient(self.credentials.conf('api-key'), - self.credentials.conf('secret-key'), - self.ttl) - - -class _CloudXNSLexiconClient(dns_common_lexicon.LexiconClient): - """ - Encapsulates all communication with the CloudXNS via Lexicon. - """ - - def __init__(self, api_key: str, secret_key: str, ttl: int) -> None: - super().__init__() - - config = dns_common_lexicon.build_lexicon_config('cloudxns', { - 'ttl': ttl, - }, { - 'auth_username': api_key, - 'auth_token': secret_key, - }) - - self.provider = cloudxns.Provider(config) - - def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]: - hint = None - if str(e).startswith('400 Client Error:'): - hint = 'Are your API key and Secret key values correct?' - - hint_disp = f' ({hint})' if hint else '' - - return errors.PluginError(f'Error determining zone identifier for {domain_name}: ' - f'{e}.{hint_disp}') diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/py.typed b/certbot-dns-cloudxns/certbot_dns_cloudxns/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/certbot-dns-cloudxns/docs/.gitignore b/certbot-dns-cloudxns/docs/.gitignore deleted file mode 100644 index ba65b13af..000000000 --- a/certbot-dns-cloudxns/docs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/_build/ diff --git a/certbot-dns-cloudxns/docs/Makefile b/certbot-dns-cloudxns/docs/Makefile deleted file mode 100644 index ecda13dfe..000000000 --- a/certbot-dns-cloudxns/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = certbot-dns-cloudxns -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/certbot-dns-cloudxns/docs/api.rst b/certbot-dns-cloudxns/docs/api.rst deleted file mode 100644 index ac13c3df2..000000000 --- a/certbot-dns-cloudxns/docs/api.rst +++ /dev/null @@ -1,5 +0,0 @@ -================= -API Documentation -================= - -Certbot plugins implement the Certbot plugins API, and do not otherwise have an external API. diff --git a/certbot-dns-cloudxns/docs/conf.py b/certbot-dns-cloudxns/docs/conf.py deleted file mode 100644 index f516d9a1e..000000000 --- a/certbot-dns-cloudxns/docs/conf.py +++ /dev/null @@ -1,181 +0,0 @@ -# -*- coding: utf-8 -*- -# -# certbot-dns-cloudxns documentation build configuration file, created by -# sphinx-quickstart on Wed May 10 16:05:50 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os - -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -needs_sphinx = '1.0' - -# 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.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode'] - -autodoc_member_order = 'bysource' -autodoc_default_flags = ['show-inheritance'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'certbot-dns-cloudxns' -copyright = u'2017, Certbot Project' -author = u'Certbot Project' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = u'0' -# The full version, including alpha/beta/rc tags. -release = u'0' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = 'en' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -default_role = 'py:obj' - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# - -# https://docs.readthedocs.io/en/stable/faq.html#i-want-to-use-the-read-the-docs-theme-locally -# on_rtd is whether we are on readthedocs.org -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' -if not on_rtd: # only import and set the theme if we're building docs locally - import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] -# otherwise, readthedocs.org uses their theme by default, so no need to specify it - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'certbot-dns-cloudxnsdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'certbot-dns-cloudxns.tex', u'certbot-dns-cloudxns Documentation', - u'Certbot Project', 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'certbot-dns-cloudxns', u'certbot-dns-cloudxns Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'certbot-dns-cloudxns', u'certbot-dns-cloudxns Documentation', - author, 'certbot-dns-cloudxns', 'One line description of project.', - 'Miscellaneous'), -] - - - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = { - 'python': ('https://docs.python.org/', None), - 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), - 'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None), -} diff --git a/certbot-dns-cloudxns/docs/index.rst b/certbot-dns-cloudxns/docs/index.rst deleted file mode 100644 index 83c6ca18d..000000000 --- a/certbot-dns-cloudxns/docs/index.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. certbot-dns-cloudxns documentation master file, created by - sphinx-quickstart on Wed May 10 16:05:50 2017. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to certbot-dns-cloudxns's documentation! -================================================ - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - -.. automodule:: certbot_dns_cloudxns - :members: - -.. toctree:: - :maxdepth: 1 - - api - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/certbot-dns-cloudxns/docs/make.bat b/certbot-dns-cloudxns/docs/make.bat deleted file mode 100644 index dddd6db56..000000000 --- a/certbot-dns-cloudxns/docs/make.bat +++ /dev/null @@ -1,36 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build -set SPHINXPROJ=certbot-dns-cloudxns - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff --git a/certbot-dns-cloudxns/readthedocs.org.requirements.txt b/certbot-dns-cloudxns/readthedocs.org.requirements.txt deleted file mode 100644 index c1754a936..000000000 --- a/certbot-dns-cloudxns/readthedocs.org.requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -# readthedocs.org gives no way to change the install command to "pip -# install -e certbot-dns-cloudxns[docs]" (that would in turn install documentation -# dependencies), but it allows to specify a requirements.txt file at -# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259) - -# Although ReadTheDocs certainly doesn't need to install the project -# in --editable mode (-e), just "pip install certbot-dns-cloudxns[docs]" does not work as -# expected and "pip install -e certbot-dns-cloudxns[docs]" must be used instead - -# We also pin our dependencies for increased stability. - --c ../tools/requirements.txt --e acme --e certbot --e certbot-dns-cloudxns[docs] diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py deleted file mode 100644 index acb3dd4cc..000000000 --- a/certbot-dns-cloudxns/setup.py +++ /dev/null @@ -1,73 +0,0 @@ -import os -import sys - -from setuptools import find_packages -from setuptools import setup - -version = '1.30.0.dev0' - -install_requires = [ - 'dns-lexicon>=3.2.1', - 'setuptools>=41.6.0', -] - -if not os.environ.get('SNAP_BUILD'): - install_requires.extend([ - # We specify the minimum acme and certbot version as the current plugin - # version for simplicity. See - # https://github.com/certbot/certbot/issues/8761 for more info. - f'acme>={version}', - f'certbot>={version}', - ]) -elif 'bdist_wheel' in sys.argv[1:]: - raise RuntimeError('Unset SNAP_BUILD when building wheels ' - 'to include certbot dependencies.') -if os.environ.get('SNAP_BUILD'): - install_requires.append('packaging') - -docs_extras = [ - 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags - 'sphinx_rtd_theme', -] - -setup( - name='certbot-dns-cloudxns', - version=version, - description="CloudXNS DNS Authenticator plugin for Certbot", - url='https://github.com/certbot/certbot', - author="Certbot Project", - author_email='certbot-dev@eff.org', - license='Apache License 2.0', - python_requires='>=3.7', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Plugins', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Security', - 'Topic :: System :: Installation/Setup', - 'Topic :: System :: Networking', - 'Topic :: System :: Systems Administration', - 'Topic :: Utilities', - ], - - packages=find_packages(), - include_package_data=True, - install_requires=install_requires, - extras_require={ - 'docs': docs_extras, - }, - entry_points={ - 'certbot.plugins': [ - 'dns-cloudxns = certbot_dns_cloudxns._internal.dns_cloudxns:Authenticator', - ], - }, -) diff --git a/certbot-dns-cloudxns/tests/dns_cloudxns_test.py b/certbot-dns-cloudxns/tests/dns_cloudxns_test.py deleted file mode 100644 index 81dea5ca4..000000000 --- a/certbot-dns-cloudxns/tests/dns_cloudxns_test.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Tests for certbot_dns_cloudxns._internal.dns_cloudxns.""" - -import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore -from requests.exceptions import HTTPError -from requests.exceptions import RequestException - -from certbot.compat import os -from certbot.plugins import dns_test_common -from certbot.plugins import dns_test_common_lexicon -from certbot.tests import util as test_util - -DOMAIN_NOT_FOUND = Exception('No domain found') -GENERIC_ERROR = RequestException -LOGIN_ERROR = HTTPError('400 Client Error: ...') - -API_KEY = 'foo' -SECRET = 'bar' - - -class AuthenticatorTest(test_util.TempDirTestCase, - dns_test_common_lexicon.BaseLexiconAuthenticatorTest): - - def setUp(self): - super().setUp() - - from certbot_dns_cloudxns._internal.dns_cloudxns import Authenticator - - path = os.path.join(self.tempdir, 'file.ini') - dns_test_common.write({"cloudxns_api_key": API_KEY, "cloudxns_secret_key": SECRET}, path) - - self.config = mock.MagicMock(cloudxns_credentials=path, - cloudxns_propagation_seconds=0) # don't wait during tests - - self.auth = Authenticator(self.config, "cloudxns") - - self.mock_client = mock.MagicMock() - # _get_cloudxns_client | pylint: disable=protected-access - self.auth._get_cloudxns_client = mock.MagicMock(return_value=self.mock_client) - - -class CloudXNSLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest): - - def setUp(self): - from certbot_dns_cloudxns._internal.dns_cloudxns import _CloudXNSLexiconClient - - self.client = _CloudXNSLexiconClient(API_KEY, SECRET, 0) - - self.provider_mock = mock.MagicMock() - self.client.provider = self.provider_mock - - -if __name__ == "__main__": - unittest.main() # pragma: no cover diff --git a/certbot/certbot/_internal/cli/plugins_parsing.py b/certbot/certbot/_internal/cli/plugins_parsing.py index f0a976bf4..d19825738 100644 --- a/certbot/certbot/_internal/cli/plugins_parsing.py +++ b/certbot/certbot/_internal/cli/plugins_parsing.py @@ -45,10 +45,6 @@ def _plugins_parsing(helpful: "helpful.HelpfulArgumentParser", default=flag_default("dns_cloudflare"), help=("Obtain certificates using a DNS TXT record (if you are " "using Cloudflare for DNS).")) - helpful.add(["plugins", "certonly"], "--dns-cloudxns", action="store_true", - default=flag_default("dns_cloudxns"), - help=("Obtain certificates using a DNS TXT record (if you are " - "using CloudXNS for DNS).")) helpful.add(["plugins", "certonly"], "--dns-digitalocean", action="store_true", default=flag_default("dns_digitalocean"), help=("Obtain certificates using a DNS TXT record (if you are " diff --git a/certbot/certbot/_internal/constants.py b/certbot/certbot/_internal/constants.py index 22bba0607..ce0b84d2e 100644 --- a/certbot/certbot/_internal/constants.py +++ b/certbot/certbot/_internal/constants.py @@ -112,7 +112,6 @@ CLI_DEFAULTS: Dict[str, Any] = dict( # noqa manual=False, webroot=False, dns_cloudflare=False, - dns_cloudxns=False, dns_digitalocean=False, dns_dnsimple=False, dns_dnsmadeeasy=False, diff --git a/certbot/certbot/_internal/plugins/disco.py b/certbot/certbot/_internal/plugins/disco.py index 30409aff0..707f5ae9d 100644 --- a/certbot/certbot/_internal/plugins/disco.py +++ b/certbot/certbot/_internal/plugins/disco.py @@ -31,7 +31,6 @@ PREFIX_FREE_DISTRIBUTIONS = [ "certbot", "certbot-apache", "certbot-dns-cloudflare", - "certbot-dns-cloudxns", "certbot-dns-digitalocean", "certbot-dns-dnsimple", "certbot-dns-dnsmadeeasy", diff --git a/certbot/certbot/_internal/plugins/selection.py b/certbot/certbot/_internal/plugins/selection.py index cde6bd221..826e1c932 100644 --- a/certbot/certbot/_internal/plugins/selection.py +++ b/certbot/certbot/_internal/plugins/selection.py @@ -168,7 +168,7 @@ def choose_plugin(prepared: List[disco.PluginEntryPoint], return None -noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns", +noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-gehirn", "dns-google", "dns-linode", "dns-luadns", "dns-nsone", "dns-ovh", "dns-rfc2136", "dns-route53", "dns-sakuracloud"] @@ -316,8 +316,6 @@ def cli_plugin_requests(config: configuration.NamespaceConfig req_auth = set_configurator(req_auth, "manual") if config.dns_cloudflare: req_auth = set_configurator(req_auth, "dns-cloudflare") - if config.dns_cloudxns: - req_auth = set_configurator(req_auth, "dns-cloudxns") if config.dns_digitalocean: req_auth = set_configurator(req_auth, "dns-digitalocean") if config.dns_dnsimple: diff --git a/certbot/docs/packaging.rst b/certbot/docs/packaging.rst index a1fb23100..75349ad14 100644 --- a/certbot/docs/packaging.rst +++ b/certbot/docs/packaging.rst @@ -12,7 +12,6 @@ We release packages and upload them to PyPI (wheels and source tarballs). - https://pypi.python.org/pypi/certbot-apache - https://pypi.python.org/pypi/certbot-nginx - https://pypi.python.org/pypi/certbot-dns-cloudflare -- https://pypi.python.org/pypi/certbot-dns-cloudxns - https://pypi.python.org/pypi/certbot-dns-digitalocean - https://pypi.python.org/pypi/certbot-dns-dnsimple - https://pypi.python.org/pypi/certbot-dns-dnsmadeeasy diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst index c4f55bc02..c43a006fd 100644 --- a/certbot/docs/using.rst +++ b/certbot/docs/using.rst @@ -206,7 +206,6 @@ use the DNS plugins on your system. Once installed, you can find documentation on how to use each plugin at: * `certbot-dns-cloudflare `_ -* `certbot-dns-cloudxns `_ * `certbot-dns-digitalocean `_ * `certbot-dns-dnsimple `_ * `certbot-dns-dnsmadeeasy `_ diff --git a/tools/_release.sh b/tools/_release.sh index a9ed017a7..368ed21d5 100755 --- a/tools/_release.sh +++ b/tools/_release.sh @@ -57,7 +57,7 @@ export GPG_TTY=$(tty) PORT=${PORT:-1234} # subpackages to be released (the way the script thinks about them) -SUBPKGS_NO_CERTBOT="acme certbot-apache certbot-nginx certbot-dns-cloudflare certbot-dns-cloudxns \ +SUBPKGS_NO_CERTBOT="acme certbot-apache certbot-nginx certbot-dns-cloudflare \ certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy \ certbot-dns-gehirn certbot-dns-google certbot-dns-linode certbot-dns-luadns \ certbot-dns-nsone certbot-dns-ovh certbot-dns-rfc2136 certbot-dns-route53 \ diff --git a/tools/docker/lib/common b/tools/docker/lib/common index 4ba86cac0..bf3598f92 100644 --- a/tools/docker/lib/common +++ b/tools/docker/lib/common @@ -20,7 +20,6 @@ export CERTBOT_PLUGINS=( "dns-dnsimple" "dns-ovh" "dns-cloudflare" - "dns-cloudxns" "dns-digitalocean" "dns-google" "dns-luadns" diff --git a/tools/pinning/current/pyproject.toml b/tools/pinning/current/pyproject.toml index b5959bc20..e8358a926 100644 --- a/tools/pinning/current/pyproject.toml +++ b/tools/pinning/current/pyproject.toml @@ -15,7 +15,6 @@ python = "^3.7" certbot-ci = {path = "../../../certbot-ci"} certbot-compatibility-test = {path = "../../../certbot-compatibility-test"} certbot-dns-cloudflare = {path = "../../../certbot-dns-cloudflare", extras = ["docs"]} -certbot-dns-cloudxns = {path = "../../../certbot-dns-cloudxns", extras = ["docs"]} certbot-dns-digitalocean = {path = "../../../certbot-dns-digitalocean", extras = ["docs"]} certbot-dns-dnsimple = {path = "../../../certbot-dns-dnsimple", extras = ["docs"]} certbot-dns-dnsmadeeasy = {path = "../../../certbot-dns-dnsmadeeasy", extras = ["docs"]} diff --git a/tools/pinning/oldest/pyproject.toml b/tools/pinning/oldest/pyproject.toml index 5585a39f4..9b76fdbb5 100644 --- a/tools/pinning/oldest/pyproject.toml +++ b/tools/pinning/oldest/pyproject.toml @@ -18,7 +18,6 @@ python = "3.7" # on acme so certbot must be listed before acme. certbot-ci = {path = "../../../certbot-ci"} certbot-dns-cloudflare = {path = "../../../certbot-dns-cloudflare"} -certbot-dns-cloudxns = {path = "../../../certbot-dns-cloudxns"} certbot-dns-digitalocean = {path = "../../../certbot-dns-digitalocean"} certbot-dns-dnsimple = {path = "../../../certbot-dns-dnsimple"} certbot-dns-dnsmadeeasy = {path = "../../../certbot-dns-dnsmadeeasy"} diff --git a/tools/venv.py b/tools/venv.py index 2aa5b0a2d..0721b2b25 100755 --- a/tools/venv.py +++ b/tools/venv.py @@ -27,7 +27,6 @@ REQUIREMENTS = [ '-e certbot[all]', '-e certbot-apache', '-e certbot-dns-cloudflare', - '-e certbot-dns-cloudxns', '-e certbot-dns-digitalocean', '-e certbot-dns-dnsimple', '-e certbot-dns-dnsmadeeasy', diff --git a/tox.cover.py b/tox.cover.py index 1f8f9dcce..f1e400eb8 100755 --- a/tox.cover.py +++ b/tox.cover.py @@ -7,7 +7,7 @@ import subprocess import sys DEFAULT_PACKAGES = [ - 'certbot', 'acme', 'certbot_apache', 'certbot_dns_cloudflare', 'certbot_dns_cloudxns', + 'certbot', 'acme', 'certbot_apache', 'certbot_dns_cloudflare', 'certbot_dns_digitalocean', 'certbot_dns_dnsimple', 'certbot_dns_dnsmadeeasy', 'certbot_dns_gehirn', 'certbot_dns_google', 'certbot_dns_linode', 'certbot_dns_luadns', 'certbot_dns_nsone', 'certbot_dns_ovh', 'certbot_dns_rfc2136', 'certbot_dns_route53', @@ -18,7 +18,6 @@ COVER_THRESHOLDS = { 'acme': {'linux': 100, 'windows': 99}, 'certbot_apache': {'linux': 100, 'windows': 100}, 'certbot_dns_cloudflare': {'linux': 98, 'windows': 98}, - 'certbot_dns_cloudxns': {'linux': 98, 'windows': 98}, 'certbot_dns_digitalocean': {'linux': 98, 'windows': 98}, 'certbot_dns_dnsimple': {'linux': 98, 'windows': 98}, 'certbot_dns_dnsmadeeasy': {'linux': 99, 'windows': 99}, diff --git a/tox.ini b/tox.ini index 03f638c0e..f4776328d 100644 --- a/tox.ini +++ b/tox.ini @@ -17,10 +17,10 @@ install_and_test = python {toxinidir}/tools/install_and_test.py # Packages are listed on one line because tox seems to have inconsistent # behavior with substitutions that contain line continuations, see # https://github.com/tox-dev/tox/issues/2069 for more info. -dns_packages = certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-gehirn certbot-dns-google certbot-dns-linode certbot-dns-luadns certbot-dns-nsone certbot-dns-ovh certbot-dns-rfc2136 certbot-dns-route53 certbot-dns-sakuracloud +dns_packages = certbot-dns-cloudflare certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-gehirn certbot-dns-google certbot-dns-linode certbot-dns-luadns certbot-dns-nsone certbot-dns-ovh certbot-dns-rfc2136 certbot-dns-route53 certbot-dns-sakuracloud win_all_packages = acme[test] certbot[test] {[base]dns_packages} certbot-nginx all_packages = {[base]win_all_packages} certbot-apache -source_paths = acme/acme certbot/certbot certbot-apache/certbot_apache certbot-ci/certbot_integration_tests certbot-ci/snap_integration_tests certbot-ci/windows_installer_integration_tests certbot-compatibility-test/certbot_compatibility_test certbot-dns-cloudflare/certbot_dns_cloudflare certbot-dns-cloudxns/certbot_dns_cloudxns certbot-dns-digitalocean/certbot_dns_digitalocean certbot-dns-dnsimple/certbot_dns_dnsimple certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy certbot-dns-gehirn/certbot_dns_gehirn certbot-dns-google/certbot_dns_google certbot-dns-linode/certbot_dns_linode certbot-dns-luadns/certbot_dns_luadns certbot-dns-nsone/certbot_dns_nsone certbot-dns-ovh/certbot_dns_ovh certbot-dns-rfc2136/certbot_dns_rfc2136 certbot-dns-route53/certbot_dns_route53 certbot-dns-sakuracloud/certbot_dns_sakuracloud certbot-nginx/certbot_nginx tests/lock_test.py +source_paths = acme/acme certbot/certbot certbot-apache/certbot_apache certbot-ci/certbot_integration_tests certbot-ci/snap_integration_tests certbot-ci/windows_installer_integration_tests certbot-compatibility-test/certbot_compatibility_test certbot-dns-cloudflare/certbot_dns_cloudflare certbot-dns-digitalocean/certbot_dns_digitalocean certbot-dns-dnsimple/certbot_dns_dnsimple certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy certbot-dns-gehirn/certbot_dns_gehirn certbot-dns-google/certbot_dns_google certbot-dns-linode/certbot_dns_linode certbot-dns-luadns/certbot_dns_luadns certbot-dns-nsone/certbot_dns_nsone certbot-dns-ovh/certbot_dns_ovh certbot-dns-rfc2136/certbot_dns_rfc2136 certbot-dns-route53/certbot_dns_route53 certbot-dns-sakuracloud/certbot_dns_sakuracloud certbot-nginx/certbot_nginx tests/lock_test.py [testenv] passenv = From 94bbb4c44c20bb71cf743b8de4652ab2bdea93c1 Mon Sep 17 00:00:00 2001 From: "Matthew W. Thomas" Date: Fri, 12 Aug 2022 16:03:08 -0500 Subject: [PATCH 09/29] docs: add BunnyDNS to list of 3rd-party plugins (#9375) * docs: add BunnyDNS to list of 3rd-party plugins You can find the plugin here: https://github.com/mwt/certbot-dns-bunny It's for [BunnyDNS](https://bunny.net/dns/). * Update AUTHORS.md --- AUTHORS.md | 1 + certbot/docs/using.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/AUTHORS.md b/AUTHORS.md index 64f60a278..f8708dde1 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -178,6 +178,7 @@ Authors * [Mathieu Leduc-Hamel](https://github.com/mlhamel) * [Matt Bostock](https://github.com/mattbostock) * [Matthew Ames](https://github.com/SuperMatt) +* [Matthew W. Thomas](https://github.com/mwt) * [Michael Schumacher](https://github.com/schumaml) * [Michael Strache](https://github.com/Jarodiv) * [Michael Sverdlin](https://github.com/sveder) diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst index 980457141..c0510da4a 100644 --- a/certbot/docs/using.rst +++ b/certbot/docs/using.rst @@ -317,6 +317,7 @@ dns-inwx_ Y Y DNS Authentication for INWX through the XML API dns-azure_ Y N DNS Authentication using Azure DNS dns-godaddy_ Y N DNS Authentication using Godaddy DNS dns-yandexcloud_ Y N DNS Authentication using Yandex Cloud DNS +dns-bunny_ Y N DNS Authentication using BunnyDNS njalla_ Y N DNS Authentication for njalla DuckDNS_ Y N DNS Authentication for DuckDNS Porkbun_ Y N DNS Authentication for Porkbun @@ -338,6 +339,7 @@ Infomaniak_ Y N DNS Authentication using Infomaniak Domains API .. _dns-azure: https://github.com/binkhq/certbot-dns-azure .. _dns-godaddy: https://github.com/miigotu/certbot-dns-godaddy .. _dns-yandexcloud: https://github.com/PykupeJIbc/certbot-dns-yandexcloud +.. _dns-bunny: https://github.com/mwt/certbot-dns-bunny .. _njalla: https://github.com/chaptergy/certbot-dns-njalla .. _DuckDNS: https://github.com/infinityofspace/certbot_dns_duckdns .. _Porkbun: https://github.com/infinityofspace/certbot_dns_porkbun From cb632c376f17dfd75306020a17248f3c33c1ab2f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 16 Aug 2022 16:01:51 -0700 Subject: [PATCH 10/29] encourage words before code (#9377) --- .github/pull_request_template.md | 1 + certbot/docs/contributing.rst | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 53298291b..1d8b498ac 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,6 @@ ## Pull Request Checklist +- [ ] The Certbot team has recently expressed interest in reviewing a PR for this. If not, this PR may be closed due our limited resources and need to prioritize how we spend them. - [ ] If the change being made is to a [distributed component](https://certbot.eff.org/docs/contributing.html#code-components-and-layout), edit the `master` section of `certbot/CHANGELOG.md` to include a description of the change being made. - [ ] Add or update any documentation as needed to support the changes in this PR. - [ ] Include your name in `AUTHORS.md` if you like. diff --git a/certbot/docs/contributing.rst b/certbot/docs/contributing.rst index ac8488e08..c32bdc170 100644 --- a/certbot/docs/contributing.rst +++ b/certbot/docs/contributing.rst @@ -500,6 +500,9 @@ Submitting a pull request Steps: +0. We recommend you talk with us in a GitHub issue or :ref:`Mattermost ` before writing a pull request to ensure the changes you're making is + something we have the time and interest to review. 1. Write your code! When doing this, you should add :ref:`mypy type annotations ` for any functions you add or modify. You can check that you've done this correctly by running ``tox -e mypy`` on a machine that has From a81d58fa6e3ce451682529947edfd807dcd56b7b Mon Sep 17 00:00:00 2001 From: alexzorin Date: Sat, 27 Aug 2022 07:25:37 +1000 Subject: [PATCH 11/29] deprecate certbot-dns-cloudxns (#9367) --- certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py | 7 +++++++ .../certbot_dns_cloudxns/_internal/dns_cloudxns.py | 6 ++++++ certbot-dns-cloudxns/tests/dns_cloudxns_test.py | 5 ++++- certbot/CHANGELOG.md | 3 ++- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py index 0d098445c..0ba512ee4 100644 --- a/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py +++ b/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py @@ -1,4 +1,11 @@ """ +.. danger:: + The certbot-dns-cloudxns plugin is deprecated and will be removed in the next major + release of Certbot. The CloudXNS DNS service is defunct and we recommend uninstalling + the plugin. + +---------- + The `~certbot_dns_cloudxns.dns_cloudxns` plugin automates the process of completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and subsequently removing, TXT records using the CloudXNS API. diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py index 15cdf8aa8..743e8567e 100644 --- a/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py +++ b/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py @@ -3,6 +3,7 @@ import logging from typing import Any from typing import Callable from typing import Optional +import warnings from lexicon.providers import cloudxns from requests import HTTPError @@ -27,6 +28,11 @@ class Authenticator(dns_common.DNSAuthenticator): ttl = 60 def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "The CloudXNS authenticator is deprecated and will be removed in the " + "next major release of Certbot. The CloudXNS DNS service is defunct and " + "we recommend removing the plugin." + ) super().__init__(*args, **kwargs) self.credentials: Optional[CredentialsConfiguration] = None diff --git a/certbot-dns-cloudxns/tests/dns_cloudxns_test.py b/certbot-dns-cloudxns/tests/dns_cloudxns_test.py index 81dea5ca4..62cb2c28b 100644 --- a/certbot-dns-cloudxns/tests/dns_cloudxns_test.py +++ b/certbot-dns-cloudxns/tests/dns_cloudxns_test.py @@ -8,6 +8,7 @@ except ImportError: # pragma: no cover from unittest import mock # type: ignore from requests.exceptions import HTTPError from requests.exceptions import RequestException +import warnings from certbot.compat import os from certbot.plugins import dns_test_common @@ -36,7 +37,9 @@ class AuthenticatorTest(test_util.TempDirTestCase, self.config = mock.MagicMock(cloudxns_credentials=path, cloudxns_propagation_seconds=0) # don't wait during tests - self.auth = Authenticator(self.config, "cloudxns") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + self.auth = Authenticator(self.config, "cloudxns") self.mock_client = mock.MagicMock() # _get_cloudxns_client | pylint: disable=protected-access diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 18b37f396..8a46f71ba 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -10,7 +10,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). ### Changed -* +* The `certbot-dns-cloudxns` plugin is now deprecated and will be removed in the + next major release of Certbot. ### Fixed From d8e45c286dea80c108c46801cd98b2f51f263c75 Mon Sep 17 00:00:00 2001 From: alexzorin Date: Tue, 30 Aug 2022 03:05:48 +1000 Subject: [PATCH 12/29] apache: remove support for Apache 2.2 and CentOS 6 (#9354) * apache: remove support for Apache 2.2 and CentOS 6 * delete more unused code * remove unused attributes * reorganize REWRITE_HTTPS_ARGS* --- .../certbot_apache/_internal/configurator.py | 110 +- .../certbot_apache/_internal/constants.py | 8 +- .../certbot_apache/_internal/http_01.py | 33 +- .../_internal/override_centos.py | 109 -- .../certbot_apache/_internal/parser.py | 48 +- certbot-apache/tests/centos6_test.py | 228 ---- certbot-apache/tests/configurator_test.py | 49 +- certbot-apache/tests/http_01_test.py | 23 +- certbot-apache/tests/parser_test.py | 9 - .../centos6_apache/apache/httpd/conf.d/README | 9 - .../apache/httpd/conf.d/ssl.conf | 222 ---- .../apache/httpd/conf.d/test.example.com.conf | 7 - .../apache/httpd/conf.d/welcome.conf | 11 - .../apache/httpd/conf/httpd.conf | 1009 ----------------- 14 files changed, 22 insertions(+), 1853 deletions(-) delete mode 100644 certbot-apache/tests/centos6_test.py delete mode 100644 certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/README delete mode 100644 certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/ssl.conf delete mode 100644 certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/test.example.com.conf delete mode 100644 certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/welcome.conf delete mode 100644 certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf/httpd.conf diff --git a/certbot-apache/certbot_apache/_internal/configurator.py b/certbot-apache/certbot_apache/_internal/configurator.py index 82b3dcaec..4c68c5ae8 100644 --- a/certbot-apache/certbot_apache/_internal/configurator.py +++ b/certbot-apache/certbot_apache/_internal/configurator.py @@ -354,12 +354,9 @@ class ApacheConfigurator(common.Configurator): self.version = self.get_version() logger.debug('Apache version is %s', '.'.join(str(i) for i in self.version)) - if self.version < (2, 2): + if self.version < (2, 4): raise errors.NotSupportedError( "Apache Version {0} not supported.".format(str(self.version))) - elif self.version < (2, 4): - logger.warning('Support for Apache 2.2 is deprecated and will be removed in a ' - 'future release.') # Recover from previous crash before Augeas initialization to have the # correct parse tree from the get go. @@ -1176,46 +1173,6 @@ class ApacheConfigurator(common.Configurator): vhost.aliases.add(serveralias) vhost.name = servername - def is_name_vhost(self, target_addr: obj.Addr) -> bool: - """Returns if vhost is a name based vhost - - NameVirtualHost was deprecated in Apache 2.4 as all VirtualHosts are - now NameVirtualHosts. If version is earlier than 2.4, check if addr - has a NameVirtualHost directive in the Apache config - - :param certbot_apache._internal.obj.Addr target_addr: vhost address - - :returns: Success - :rtype: bool - - """ - # Mixed and matched wildcard NameVirtualHost with VirtualHost - # behavior is undefined. Make sure that an exact match exists - - # search for NameVirtualHost directive for ip_addr - # note ip_addr can be FQDN although Apache does not recommend it - return (self.version >= (2, 4) or - bool(self.parser.find_dir("NameVirtualHost", str(target_addr)))) - - def add_name_vhost(self, addr: obj.Addr) -> None: - """Adds NameVirtualHost directive for given address. - - :param addr: Address that will be added as NameVirtualHost directive - :type addr: :class:`~certbot_apache._internal.obj.Addr` - - """ - - loc = parser.get_aug_path(self.parser.loc["name"]) - if addr.get_port() == "443": - self.parser.add_dir_to_ifmodssl( - loc, "NameVirtualHost", [str(addr)]) - else: - self.parser.add_dir(loc, "NameVirtualHost", [str(addr)]) - - msg = "Setting {0} to be NameBasedVirtualHost\n".format(addr) - logger.debug(msg) - self.save_notes += msg - def prepare_server_https(self, port: str, temp: bool = False) -> None: """Prepare the server for HTTPS. @@ -1363,8 +1320,7 @@ class ApacheConfigurator(common.Configurator): """ if self.options.handle_modules: - if self.version >= (2, 4) and ("socache_shmcb_module" not in - self.parser.modules): + if "socache_shmcb_module" not in self.parser.modules: self.enable_mod("socache_shmcb", temp=temp) if "ssl_module" not in self.parser.modules: self.enable_mod("ssl", temp=temp) @@ -1451,10 +1407,6 @@ class ApacheConfigurator(common.Configurator): # for the new directives; For these reasons... this is tacked # on after fully creating the new vhost - # Now check if addresses need to be added as NameBasedVhost addrs - # This is for compliance with versions of Apache < 2.4 - self._add_name_vhost_if_necessary(ssl_vhost) - return ssl_vhost def _get_new_vh_path(self, orig_matches: List[str], new_matches: List[str]) -> Optional[str]: @@ -1753,40 +1705,6 @@ class ApacheConfigurator(common.Configurator): aliases = (self.parser.aug.get(match) for match in matches) return self.domain_in_names(aliases, target_name) - def _add_name_vhost_if_necessary(self, vhost: obj.VirtualHost) -> None: - """Add NameVirtualHost Directives if necessary for new vhost. - - NameVirtualHosts was a directive in Apache < 2.4 - https://httpd.apache.org/docs/2.2/mod/core.html#namevirtualhost - - :param vhost: New virtual host that was recently created. - :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost` - - """ - need_to_save: bool = False - - # See if the exact address appears in any other vhost - # Remember 1.1.1.1:* == 1.1.1.1 -> hence any() - for addr in vhost.addrs: - # In Apache 2.2, when a NameVirtualHost directive is not - # set, "*" and "_default_" will conflict when sharing a port - addrs = {addr,} - if addr.get_addr() in ("*", "_default_"): - addrs.update(obj.Addr((a, addr.get_port(),)) - for a in ("*", "_default_")) - - for test_vh in self.vhosts: - if (vhost.filep != test_vh.filep and - any(test_addr in addrs for - test_addr in test_vh.addrs) and not self.is_name_vhost(addr)): - self.add_name_vhost(addr) - logger.info("Enabling NameVirtualHosts on %s", addr) - need_to_save = True - break - - if need_to_save: - self.save() - def find_vhost_by_id(self, id_str: str) -> obj.VirtualHost: """ Searches through VirtualHosts and tries to match the id in a comment @@ -2002,12 +1920,6 @@ class ApacheConfigurator(common.Configurator): :param unused_options: Not currently used :type unused_options: Not Available """ - min_apache_ver = (2, 3, 3) - if self.get_version() < min_apache_ver: - raise errors.PluginError( - "Unable to set OCSP directives.\n" - "Apache version is below 2.3.3.") - if "socache_shmcb_module" not in self.parser.modules: self.enable_mod("socache_shmcb") @@ -2188,10 +2100,7 @@ class ApacheConfigurator(common.Configurator): general_vh.filep, ssl_vhost.filep) def _set_https_redirection_rewrite_rule(self, vhost: obj.VirtualHost) -> None: - if self.get_version() >= (2, 3, 9): - self.parser.add_dir(vhost.path, "RewriteRule", constants.REWRITE_HTTPS_ARGS_WITH_END) - else: - self.parser.add_dir(vhost.path, "RewriteRule", constants.REWRITE_HTTPS_ARGS) + self.parser.add_dir(vhost.path, "RewriteRule", constants.REWRITE_HTTPS_ARGS) def _verify_no_certbot_redirect(self, vhost: obj.VirtualHost) -> None: """Checks to see if a redirect was already installed by certbot. @@ -2223,9 +2132,6 @@ class ApacheConfigurator(common.Configurator): rewrite_args_dict[dir_path].append(match) if rewrite_args_dict: - redirect_args = [constants.REWRITE_HTTPS_ARGS, - constants.REWRITE_HTTPS_ARGS_WITH_END] - for dir_path, args_paths in rewrite_args_dict.items(): arg_vals = [self.parser.aug.get(x) for x in args_paths] @@ -2237,7 +2143,7 @@ class ApacheConfigurator(common.Configurator): raise errors.PluginEnhancementAlreadyPresent( "Certbot has already enabled redirection") - if arg_vals in redirect_args: + if arg_vals == constants.REWRITE_HTTPS_ARGS: raise errors.PluginEnhancementAlreadyPresent( "Certbot has already enabled redirection") @@ -2306,12 +2212,6 @@ class ApacheConfigurator(common.Configurator): if ssl_vhost.aliases: serveralias = "ServerAlias " + " ".join(ssl_vhost.aliases) - rewrite_rule_args: List[str] - if self.get_version() >= (2, 3, 9): - rewrite_rule_args = constants.REWRITE_HTTPS_ARGS_WITH_END - else: - rewrite_rule_args = constants.REWRITE_HTTPS_ARGS - return ( f"\n" f"{servername} \n" @@ -2319,7 +2219,7 @@ class ApacheConfigurator(common.Configurator): f"ServerSignature Off\n" f"\n" f"RewriteEngine On\n" - f"RewriteRule {' '.join(rewrite_rule_args)}\n" + f"RewriteRule {' '.join(constants.REWRITE_HTTPS_ARGS)}\n" "\n" f"ErrorLog {self.options.logs_root}/redirect.error.log\n" f"LogLevel warn\n" diff --git a/certbot-apache/certbot_apache/_internal/constants.py b/certbot-apache/certbot_apache/_internal/constants.py index 4e6fa1791..0861e2da9 100644 --- a/certbot-apache/certbot_apache/_internal/constants.py +++ b/certbot-apache/certbot_apache/_internal/constants.py @@ -42,18 +42,14 @@ AUGEAS_LENS_DIR = pkg_resources.resource_filename( """Path to the Augeas lens directory""" REWRITE_HTTPS_ARGS: List[str] = [ - "^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,NE,R=permanent]"] -"""Apache version<2.3.9 rewrite rule arguments used for redirections to -https vhost""" - -REWRITE_HTTPS_ARGS_WITH_END: List[str] = [ "^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[END,NE,R=permanent]"] """Apache version >= 2.3.9 rewrite rule arguments used for redirections to https vhost""" OLD_REWRITE_HTTPS_ARGS: List[List[str]] = [ ["^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,QSA,R=permanent]"], - ["^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[END,QSA,R=permanent]"]] + ["^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[END,QSA,R=permanent]"], + ["^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,NE,R=permanent]"]] HSTS_ARGS: List[str] = ["always", "set", "Strict-Transport-Security", "\"max-age=31536000\""] diff --git a/certbot-apache/certbot_apache/_internal/http_01.py b/certbot-apache/certbot_apache/_internal/http_01.py index ade2265ea..bceec2c32 100644 --- a/certbot-apache/certbot_apache/_internal/http_01.py +++ b/certbot-apache/certbot_apache/_internal/http_01.py @@ -24,22 +24,6 @@ logger = logging.getLogger(__name__) class ApacheHttp01(common.ChallengePerformer): """Class that performs HTTP-01 challenges within the Apache configurator.""" - CONFIG_TEMPLATE22_PRE = """\ - RewriteEngine on - RewriteRule ^/\\.well-known/acme-challenge/([A-Za-z0-9-_=]+)$ {0}/$1 [L] - - """ - CONFIG_TEMPLATE22_POST = """\ - - Order Allow,Deny - Allow from all - - - Order Allow,Deny - Allow from all - - """ - CONFIG_TEMPLATE24_PRE = """\ RewriteEngine on RewriteRule ^/\\.well-known/acme-challenge/([A-Za-z0-9-_=]+)$ {0}/$1 [END] @@ -90,11 +74,7 @@ class ApacheHttp01(common.ChallengePerformer): """Make sure that we have the needed modules available for http01""" if self.configurator.conf("handle-modules"): - needed_modules = ["rewrite"] - if self.configurator.version < (2, 4): - needed_modules.append("authz_host") - else: - needed_modules.append("authz_core") + needed_modules = ["rewrite", "authz_core"] for mod in needed_modules: if mod + "_module" not in self.configurator.parser.modules: self.configurator.enable_mod(mod, temp=True) @@ -131,15 +111,8 @@ class ApacheHttp01(common.ChallengePerformer): self.configurator.reverter.register_file_creation( True, self.challenge_conf_post) - if self.configurator.version < (2, 4): - config_template_pre = self.CONFIG_TEMPLATE22_PRE - config_template_post = self.CONFIG_TEMPLATE22_POST - else: - config_template_pre = self.CONFIG_TEMPLATE24_PRE - config_template_post = self.CONFIG_TEMPLATE24_POST - - config_text_pre = config_template_pre.format(self.challenge_dir) - config_text_post = config_template_post.format(self.challenge_dir) + config_text_pre = self.CONFIG_TEMPLATE24_PRE.format(self.challenge_dir) + config_text_post = self.CONFIG_TEMPLATE24_POST.format(self.challenge_dir) logger.debug("writing a pre config file with text:\n %s", config_text_pre) with open(self.challenge_conf_pre, "w") as new_conf: diff --git a/certbot-apache/certbot_apache/_internal/override_centos.py b/certbot-apache/certbot_apache/_internal/override_centos.py index a436e8457..f9de9fad9 100644 --- a/certbot-apache/certbot_apache/_internal/override_centos.py +++ b/certbot-apache/certbot_apache/_internal/override_centos.py @@ -1,8 +1,6 @@ """ Distribution specific override class for CentOS family (RHEL, Fedora) """ import logging from typing import Any -from typing import cast -from typing import List from certbot_apache._internal import apache_util from certbot_apache._internal import configurator @@ -11,7 +9,6 @@ from certbot_apache._internal.configurator import OsOptions from certbot import errors from certbot import util -from certbot.errors import MisconfigurationError logger = logging.getLogger(__name__) @@ -79,82 +76,6 @@ class CentOSConfigurator(configurator.ApacheConfigurator): return CentOSParser( self.options.server_root, self, self.options.vhost_root, self.version) - def _deploy_cert(self, *args: Any, **kwargs: Any) -> None: # pylint: disable=arguments-differ - """ - Override _deploy_cert in order to ensure that the Apache configuration - has "LoadModule ssl_module..." before parsing the VirtualHost configuration - that was created by Certbot - """ - super()._deploy_cert(*args, **kwargs) - if self.version < (2, 4, 0): - self._deploy_loadmodule_ssl_if_needed() - - def _deploy_loadmodule_ssl_if_needed(self) -> None: - """ - Add "LoadModule ssl_module " to main httpd.conf if - it doesn't exist there already. - """ - - loadmods = self.parser.find_dir("LoadModule", "ssl_module", exclude=False) - - correct_ifmods: List[str] = [] - loadmod_args: List[str] = [] - loadmod_paths: List[str] = [] - for m in loadmods: - noarg_path = m.rpartition("/")[0] - path_args = self.parser.get_all_args(noarg_path) - if loadmod_args: - if loadmod_args != path_args: - msg = ("Certbot encountered multiple LoadModule directives " - "for LoadModule ssl_module with differing library paths. " - "Please remove or comment out the one(s) that are not in " - "use, and run Certbot again.") - raise MisconfigurationError(msg) - else: - loadmod_args = [arg for arg in path_args if arg] - - centos_parser: CentOSParser = cast(CentOSParser, self.parser) - if centos_parser.not_modssl_ifmodule(noarg_path): - if centos_parser.loc["default"] in noarg_path: - # LoadModule already in the main configuration file - if "ifmodule/" in noarg_path.lower() or "ifmodule[1]" in noarg_path.lower(): - # It's the first or only IfModule in the file - return - # Populate the list of known !mod_ssl.c IfModules - nodir_path = noarg_path.rpartition("/directive")[0] - correct_ifmods.append(nodir_path) - else: - loadmod_paths.append(noarg_path) - - if not loadmod_args: - # Do not try to enable mod_ssl - return - - # Force creation as the directive wasn't found from the beginning of - # httpd.conf - rootconf_ifmod = self.parser.create_ifmod( - parser.get_aug_path(self.parser.loc["default"]), - "!mod_ssl.c", beginning=True) - # parser.get_ifmod returns a path postfixed with "/", remove that - self.parser.add_dir(rootconf_ifmod[:-1], "LoadModule", loadmod_args) - correct_ifmods.append(rootconf_ifmod[:-1]) - self.save_notes += "Added LoadModule ssl_module to main configuration.\n" - - # Wrap LoadModule mod_ssl inside of if it's not - # configured like this already. - for loadmod_path in loadmod_paths: - nodir_path = loadmod_path.split("/directive")[0] - # Remove the old LoadModule directive - self.parser.aug.remove(loadmod_path) - - # Create a new IfModule !mod_ssl.c if not already found on path - ssl_ifmod = self.parser.get_ifmod(nodir_path, "!mod_ssl.c", beginning=True)[:-1] - if ssl_ifmod not in correct_ifmods: - self.parser.add_dir(ssl_ifmod, "LoadModule", loadmod_args) - correct_ifmods.append(ssl_ifmod) - self.save_notes += ("Wrapped pre-existing LoadModule ssl_module " - "inside of block.\n") - class CentOSParser(parser.ApacheParser): """CentOS specific ApacheParser override class""" @@ -174,33 +95,3 @@ class CentOSParser(parser.ApacheParser): defines = apache_util.parse_define_file(self.sysconfig_filep, "OPTIONS") for k, v in defines.items(): self.variables[k] = v - - def not_modssl_ifmodule(self, path: str) -> bool: - """Checks if the provided Augeas path has argument !mod_ssl""" - - if "ifmodule" not in path.lower(): - return False - - # Trim the path to the last ifmodule - workpath = path.lower() - while workpath: - # Get path to the last IfModule (ignore the tail) - parts = workpath.rpartition("ifmodule") - - if not parts[0]: - # IfModule not found - break - ifmod_path = parts[0] + parts[1] - # Check if ifmodule had an index - if parts[2].startswith("["): - # Append the index from tail - ifmod_path += parts[2].partition("/")[0] - # Get the original path trimmed to correct length - # This is required to preserve cases - ifmod_real_path = path[0:len(ifmod_path)] - if "!mod_ssl.c" in self.get_all_args(ifmod_real_path): - return True - # Set the workpath to the heading part - workpath = parts[0] - - return False diff --git a/certbot-apache/certbot_apache/_internal/parser.py b/certbot-apache/certbot_apache/_internal/parser.py index 46e61843f..b122979e4 100644 --- a/certbot-apache/certbot_apache/_internal/parser.py +++ b/certbot-apache/certbot_apache/_internal/parser.py @@ -47,6 +47,7 @@ class ApacheParser: arg_var_interpreter: Pattern = re.compile(r"\$\{[^ \}]*}") fnmatch_chars: Set[str] = {"*", "?", "\\", "[", "]"} + # pylint: disable=unused-argument def __init__(self, root: str, configurator: "ApacheConfigurator", vhostroot: str, version: Tuple[int, ...] = (2, 4)) -> None: # Note: Order is important here. @@ -74,9 +75,8 @@ class ApacheParser: self.loc: Dict[str, str] = {"root": self._find_config_root()} self.parse_file(self.loc["root"]) - if version >= (2, 4): - # Look up variables from httpd and add to DOM if not already parsed - self.update_runtime_variables() + # Look up variables from httpd and add to DOM if not already parsed + self.update_runtime_variables() # This problem has been fixed in Augeas 1.0 self.standardize_excl() @@ -95,11 +95,6 @@ class ApacheParser: self.parse_file(os.path.abspath(vhostroot) + "/" + self.configurator.options.vhost_files) - # check to see if there were unparsed define statements - if version < (2, 4): - if self.find_dir("Define", exclude=False): - raise errors.PluginError("Error parsing runtime variables") - def check_parsing_errors(self, lens: str) -> None: """Verify Augeas can parse all of the lens files. @@ -382,7 +377,7 @@ class ApacheParser: for i, arg in enumerate(args): self.aug.set("%s/arg[%d]" % (nvh_path, i + 1), arg) - def get_ifmod(self, aug_conf_path: str, mod: str, beginning: bool = False) -> str: + def get_ifmod(self, aug_conf_path: str, mod: str) -> str: """Returns the path to and creates one if it doesn't exist. :param str aug_conf_path: Augeas configuration path @@ -399,35 +394,26 @@ class ApacheParser: if_mods = self.aug.match(("%s/IfModule/*[self::arg='%s']" % (aug_conf_path, mod))) if not if_mods: - return self.create_ifmod(aug_conf_path, mod, beginning) + return self.create_ifmod(aug_conf_path, mod) # Strip off "arg" at end of first ifmod path return if_mods[0].rpartition("arg")[0] - def create_ifmod(self, aug_conf_path: str, mod: str, beginning: bool = False) -> str: + def create_ifmod(self, aug_conf_path: str, mod: str) -> str: """Creates a new and returns its path. :param str aug_conf_path: Augeas configuration path :param str mod: module ie. mod_ssl.c - :param bool beginning: If the IfModule should be created to the beginning - of augeas path DOM tree. :returns: Augeas path of the newly created IfModule directive. The path may be dynamic, i.e. .../IfModule[last()] :rtype: str """ - if beginning: - c_path_arg = "{}/IfModule[1]/arg".format(aug_conf_path) - # Insert IfModule before the first directive - self.aug.insert("{}/directive[1]".format(aug_conf_path), - "IfModule", True) - retpath = "{}/IfModule[1]/".format(aug_conf_path) - else: - c_path = "{}/IfModule[last() + 1]".format(aug_conf_path) - c_path_arg = "{}/IfModule[last()]/arg".format(aug_conf_path) - self.aug.set(c_path, "") - retpath = "{}/IfModule[last()]/".format(aug_conf_path) + c_path = "{}/IfModule[last() + 1]".format(aug_conf_path) + c_path_arg = "{}/IfModule[last()]/arg".format(aug_conf_path) + self.aug.set(c_path, "") + retpath = "{}/IfModule[last()]/".format(aug_conf_path) self.aug.set(c_path_arg, mod) return retpath @@ -587,20 +573,6 @@ class ApacheParser: return ordered_matches - def get_all_args(self, match: str) -> List[Optional[str]]: - """ - Tries to fetch all arguments for a directive. See get_arg. - - Note that if match is an ancestor node, it returns all names of - child directives as well as the list of arguments. - - """ - - if match[-1] != "/": - match = match + "/" - allargs = self.aug.match(match + '*') - return [self.get_arg(arg) for arg in allargs] - def get_arg(self, match: str) -> Optional[str]: """Uses augeas.get to get argument value and interprets result. diff --git a/certbot-apache/tests/centos6_test.py b/certbot-apache/tests/centos6_test.py deleted file mode 100644 index 85f1333e9..000000000 --- a/certbot-apache/tests/centos6_test.py +++ /dev/null @@ -1,228 +0,0 @@ -"""Test for certbot_apache._internal.configurator for CentOS 6 overrides""" -import unittest -from unittest import mock - -from certbot.compat import os -from certbot.errors import MisconfigurationError -from certbot_apache._internal import obj -from certbot_apache._internal import override_centos -from certbot_apache._internal import parser -import util - - -def get_vh_truth(temp_dir, config_name): - """Return the ground truth for the specified directory.""" - prefix = os.path.join( - temp_dir, config_name, "httpd/conf.d") - - aug_pre = "/files" + prefix - vh_truth = [ - obj.VirtualHost( - os.path.join(prefix, "test.example.com.conf"), - os.path.join(aug_pre, "test.example.com.conf/VirtualHost"), - {obj.Addr.fromstring("*:80")}, - False, True, "test.example.com"), - obj.VirtualHost( - os.path.join(prefix, "ssl.conf"), - os.path.join(aug_pre, "ssl.conf/VirtualHost"), - {obj.Addr.fromstring("_default_:443")}, - True, True, None) - ] - return vh_truth - -class CentOS6Tests(util.ApacheTest): - """Tests for CentOS 6""" - - def setUp(self): # pylint: disable=arguments-differ - test_dir = "centos6_apache/apache" - config_root = "centos6_apache/apache/httpd" - vhost_root = "centos6_apache/apache/httpd/conf.d" - super().setUp(test_dir=test_dir, - config_root=config_root, - vhost_root=vhost_root) - - self.config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, self.work_dir, - version=(2, 2, 15), os_info="centos") - self.vh_truth = get_vh_truth( - self.temp_dir, "centos6_apache/apache") - - def test_get_parser(self): - self.assertIsInstance(self.config.parser, override_centos.CentOSParser) - - def test_get_virtual_hosts(self): - """Make sure all vhosts are being properly found.""" - vhs = self.config.get_virtual_hosts() - self.assertEqual(len(vhs), 2) - found = 0 - - for vhost in vhs: - for centos_truth in self.vh_truth: - if vhost == centos_truth: - found += 1 - break - else: - raise Exception("Missed: %s" % vhost) # pragma: no cover - self.assertEqual(found, 2) - - @mock.patch("certbot_apache._internal.configurator.display_util.notify") - def test_loadmod_default(self, unused_mock_notify): - ssl_loadmods = self.config.parser.find_dir( - "LoadModule", "ssl_module", exclude=False) - self.assertEqual(len(ssl_loadmods), 1) - # Make sure the LoadModule ssl_module is in ssl.conf (default) - self.assertIn("ssl.conf", ssl_loadmods[0]) - # ...and that it's not inside of - self.assertNotIn("IfModule", ssl_loadmods[0]) - - # Get the example vhost - self.config.assoc["test.example.com"] = self.vh_truth[0] - self.config.deploy_cert( - "random.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - self.config.save() - - post_loadmods = self.config.parser.find_dir( - "LoadModule", "ssl_module", exclude=False) - - # We should now have LoadModule ssl_module in root conf and ssl.conf - self.assertEqual(len(post_loadmods), 2) - for lm in post_loadmods: - # lm[:-7] removes "/arg[#]" from the path - arguments = self.config.parser.get_all_args(lm[:-7]) - self.assertEqual(arguments, ["ssl_module", "modules/mod_ssl.so"]) - # ...and both of them should be wrapped in - # lm[:-17] strips off /directive/arg[1] from the path. - ifmod_args = self.config.parser.get_all_args(lm[:-17]) - self.assertIn("!mod_ssl.c", ifmod_args) - - @mock.patch("certbot_apache._internal.configurator.display_util.notify") - def test_loadmod_multiple(self, unused_mock_notify): - sslmod_args = ["ssl_module", "modules/mod_ssl.so"] - # Adds another LoadModule to main httpd.conf in addtition to ssl.conf - self.config.parser.add_dir(self.config.parser.loc["default"], "LoadModule", - sslmod_args) - self.config.save() - pre_loadmods = self.config.parser.find_dir( - "LoadModule", "ssl_module", exclude=False) - # LoadModules are not within IfModule blocks - self.assertIs(any("ifmodule" in m.lower() for m in pre_loadmods), False) - self.config.assoc["test.example.com"] = self.vh_truth[0] - self.config.deploy_cert( - "random.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - post_loadmods = self.config.parser.find_dir( - "LoadModule", "ssl_module", exclude=False) - - for mod in post_loadmods: - with self.subTest(mod=mod): - # pylint: disable=no-member - self.assertIs(self.config.parser.not_modssl_ifmodule(mod), True) - - @mock.patch("certbot_apache._internal.configurator.display_util.notify") - def test_loadmod_rootconf_exists(self, unused_mock_notify): - sslmod_args = ["ssl_module", "modules/mod_ssl.so"] - rootconf_ifmod = self.config.parser.get_ifmod( - parser.get_aug_path(self.config.parser.loc["default"]), - "!mod_ssl.c", beginning=True) - self.config.parser.add_dir(rootconf_ifmod[:-1], "LoadModule", sslmod_args) - self.config.save() - # Get the example vhost - self.config.assoc["test.example.com"] = self.vh_truth[0] - self.config.deploy_cert( - "random.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - self.config.save() - - root_loadmods = self.config.parser.find_dir( - "LoadModule", "ssl_module", - start=parser.get_aug_path(self.config.parser.loc["default"]), - exclude=False) - - mods = [lm for lm in root_loadmods if self.config.parser.loc["default"] in lm] - - self.assertEqual(len(mods), 1) - # [:-7] removes "/arg[#]" from the path - self.assertEqual( - self.config.parser.get_all_args(mods[0][:-7]), - sslmod_args) - - @mock.patch("certbot_apache._internal.configurator.display_util.notify") - def test_neg_loadmod_already_on_path(self, unused_mock_notify): - loadmod_args = ["ssl_module", "modules/mod_ssl.so"] - ifmod = self.config.parser.get_ifmod( - self.vh_truth[1].path, "!mod_ssl.c", beginning=True) - self.config.parser.add_dir(ifmod[:-1], "LoadModule", loadmod_args) - self.config.parser.add_dir(self.vh_truth[1].path, "LoadModule", loadmod_args) - self.config.save() - pre_loadmods = self.config.parser.find_dir( - "LoadModule", "ssl_module", start=self.vh_truth[1].path, exclude=False) - self.assertEqual(len(pre_loadmods), 2) - # The ssl.conf now has two LoadModule directives, one inside of - # !mod_ssl.c IfModule - self.config.assoc["test.example.com"] = self.vh_truth[0] - self.config.deploy_cert( - "random.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - self.config.save() - # Ensure that the additional LoadModule wasn't written into the IfModule - post_loadmods = self.config.parser.find_dir( - "LoadModule", "ssl_module", start=self.vh_truth[1].path, exclude=False) - self.assertEqual(len(post_loadmods), 1) - - def test_loadmod_non_duplicate(self): - # the modules/mod_ssl.so exists in ssl.conf - sslmod_args = ["ssl_module", "modules/mod_somethingelse.so"] - rootconf_ifmod = self.config.parser.get_ifmod( - parser.get_aug_path(self.config.parser.loc["default"]), - "!mod_ssl.c", beginning=True) - self.config.parser.add_dir(rootconf_ifmod[:-1], "LoadModule", sslmod_args) - self.config.save() - self.config.assoc["test.example.com"] = self.vh_truth[0] - pre_matches = self.config.parser.find_dir("LoadModule", - "ssl_module", exclude=False) - - self.assertRaises(MisconfigurationError, self.config.deploy_cert, - "random.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - - post_matches = self.config.parser.find_dir("LoadModule", - "ssl_module", exclude=False) - # Make sure that none was changed - self.assertEqual(pre_matches, post_matches) - - @mock.patch("certbot_apache._internal.configurator.display_util.notify") - def test_loadmod_not_found(self, unused_mock_notify): - # Remove all existing LoadModule ssl_module... directives - orig_loadmods = self.config.parser.find_dir("LoadModule", - "ssl_module", - exclude=False) - for mod in orig_loadmods: - noarg_path = mod.rpartition("/")[0] - self.config.parser.aug.remove(noarg_path) - self.config.save() - self.config.deploy_cert( - "random.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - - post_loadmods = self.config.parser.find_dir("LoadModule", - "ssl_module", - exclude=False) - self.assertEqual(post_loadmods, []) - - def test_no_ifmod_search_false(self): - #pylint: disable=no-member - - self.assertIs(self.config.parser.not_modssl_ifmodule( - "/path/does/not/include/ifmod" - ), False) - self.assertIs(self.config.parser.not_modssl_ifmodule( - "" - ), False) - self.assertIs(self.config.parser.not_modssl_ifmodule( - "/path/includes/IfModule/but/no/arguments" - ), False) - - -if __name__ == "__main__": - unittest.main() # pragma: no cover diff --git a/certbot-apache/tests/configurator_test.py b/certbot-apache/tests/configurator_test.py index 3a557bb71..566907506 100644 --- a/certbot-apache/tests/configurator_test.py +++ b/certbot-apache/tests/configurator_test.py @@ -443,18 +443,6 @@ class MultipleVhostsTest(util.ApacheTest): "SSLCertificateChainFile", "two/cert_chain.pem", self.vh_truth[1].path)) - def test_is_name_vhost(self): - addr = obj.Addr.fromstring("*:80") - self.assertIs(self.config.is_name_vhost(addr), True) - self.config.version = (2, 2) - self.assertIs(self.config.is_name_vhost(addr), False) - - def test_add_name_vhost(self): - self.config.add_name_vhost(obj.Addr.fromstring("*:443")) - self.config.add_name_vhost(obj.Addr.fromstring("*:80")) - self.assertTrue(self.config.parser.find_dir("NameVirtualHost", "*:443", exclude=False)) - self.assertTrue(self.config.parser.find_dir("NameVirtualHost", "*:80")) - def test_add_listen_80(self): mock_find = mock.Mock() mock_add_dir = mock.Mock() @@ -642,9 +630,6 @@ class MultipleVhostsTest(util.ApacheTest): self.assertIs(ssl_vhost.ssl, True) self.assertIs(ssl_vhost.enabled, False) - self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]), - self.config.is_name_vhost(ssl_vhost)) - self.assertEqual(len(self.config.vhosts), 13) def test_clean_vhost_ssl(self): @@ -721,21 +706,6 @@ class MultipleVhostsTest(util.ApacheTest): # pylint: disable=protected-access self.assertIs(self.config._get_ssl_vhost_path("example_path").endswith(".conf"), True) - def test_add_name_vhost_if_necessary(self): - # pylint: disable=protected-access - self.config.add_name_vhost = mock.Mock() - self.config.version = (2, 2) - self.config._add_name_vhost_if_necessary(self.vh_truth[0]) - self.assertIs(self.config.add_name_vhost.called, True) - - new_addrs = set() - for addr in self.vh_truth[0].addrs: - new_addrs.add(obj.Addr(("_default_", addr.get_port(),))) - - self.vh_truth[0].addrs = new_addrs - self.config._add_name_vhost_if_necessary(self.vh_truth[0]) - self.assertEqual(self.config.add_name_vhost.call_count, 2) - @mock.patch("certbot_apache._internal.configurator.http_01.ApacheHttp01.perform") @mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.restart") def test_perform(self, mock_restart, mock_http_perform): @@ -946,20 +916,6 @@ class MultipleVhostsTest(util.ApacheTest): self.assertEqual(len(stapling_cache_aug_path), 1) - - @mock.patch("certbot.util.exe_exists") - def test_ocsp_unsupported_apache_version(self, mock_exe): - mock_exe.return_value = True - self.config.parser.update_runtime_variables = mock.Mock() - self.config.parser.modules["mod_ssl.c"] = None - self.config.parser.modules["socache_shmcb_module"] = None - self.config.get_version = mock.Mock(return_value=(2, 2, 0)) - self.config.choose_vhost("certbot.demo") - - self.assertRaises(errors.PluginError, - self.config.enhance, "certbot.demo", "staple-ocsp") - - def test_get_http_vhost_third_filter(self): ssl_vh = obj.VirtualHost( "fp", "ap", {obj.Addr(("*", "443"))}, @@ -1137,7 +1093,7 @@ class MultipleVhostsTest(util.ApacheTest): self.config.parser.modules["rewrite_module"] = None self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True - self.config.get_version = mock.Mock(return_value=(2, 2, 0)) + self.config.get_version = mock.Mock(return_value=(2, 4, 0)) ssl_vhost = self.config.choose_vhost("certbot.demo") @@ -1567,9 +1523,6 @@ class MultiVhostsTest(util.ApacheTest): self.assertIs(ssl_vhost.ssl, True) self.assertIs(ssl_vhost.enabled, False) - self.assertEqual(self.config.is_name_vhost(self.vh_truth[1]), - self.config.is_name_vhost(ssl_vhost)) - mock_path = "certbot_apache._internal.configurator.ApacheConfigurator._get_new_vh_path" with mock.patch(mock_path) as mock_getpath: mock_getpath.return_value = None diff --git a/certbot-apache/tests/http_01_test.py b/certbot-apache/tests/http_01_test.py index 9085f68dc..65dfb6344 100644 --- a/certbot-apache/tests/http_01_test.py +++ b/certbot-apache/tests/http_01_test.py @@ -53,15 +53,6 @@ class ApacheHttp01Test(util.ApacheTest): def test_empty_perform(self): self.assertEqual(len(self.http.perform()), 0) - @mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.enable_mod") - def test_enable_modules_apache_2_2(self, mock_enmod): - self.config.version = (2, 2) - del self.config.parser.modules["authz_host_module"] - del self.config.parser.modules["mod_authz_host.c"] - - enmod_calls = self.common_enable_modules_test(mock_enmod) - self.assertEqual(enmod_calls[0][0][0], "authz_host") - @mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.enable_mod") def test_enable_modules_apache_2_4(self, mock_enmod): del self.config.parser.modules["authz_core_module"] @@ -143,21 +134,12 @@ class ApacheHttp01Test(util.ApacheTest): self.config.config.http01_port = 12345 self.assertRaises(errors.PluginError, self.http.perform) - def test_perform_1_achall_apache_2_2(self): - self.combinations_perform_test(num_achalls=1, minor_version=2) - def test_perform_1_achall_apache_2_4(self): self.combinations_perform_test(num_achalls=1, minor_version=4) - def test_perform_2_achall_apache_2_2(self): - self.combinations_perform_test(num_achalls=2, minor_version=2) - def test_perform_2_achall_apache_2_4(self): self.combinations_perform_test(num_achalls=2, minor_version=4) - def test_perform_3_achall_apache_2_2(self): - self.combinations_perform_test(num_achalls=3, minor_version=2) - def test_perform_3_achall_apache_2_4(self): self.combinations_perform_test(num_achalls=3, minor_version=4) @@ -230,10 +212,7 @@ class ApacheHttp01Test(util.ApacheTest): self.assertIn("RewriteRule", pre_conf_contents) self.assertIn(self.http.challenge_dir, post_conf_contents) - if self.config.version < (2, 4): - self.assertIn("Allow from all", post_conf_contents) - else: - self.assertIn("Require all granted", post_conf_contents) + self.assertIn("Require all granted", post_conf_contents) def _test_challenge_file(self, achall): name = os.path.join(self.http.challenge_dir, achall.chall.encode("token")) diff --git a/certbot-apache/tests/parser_test.py b/certbot-apache/tests/parser_test.py index 1062156e3..31a769ddd 100644 --- a/certbot-apache/tests/parser_test.py +++ b/certbot-apache/tests/parser_test.py @@ -370,15 +370,6 @@ class ParserInitTest(util.ApacheTest): ApacheParser, os.path.relpath(self.config_path), self.config, "/dummy/vhostpath", version=(2, 4, 22)) - @mock.patch("certbot_apache._internal.apache_util._get_runtime_cfg") - def test_unparseable(self, mock_cfg): - from certbot_apache._internal.parser import ApacheParser - mock_cfg.return_value = ('Define: TEST') - self.assertRaises( - errors.PluginError, - ApacheParser, os.path.relpath(self.config_path), self.config, - "/dummy/vhostpath", version=(2, 2, 22)) - def test_root_normalized(self): from certbot_apache._internal.parser import ApacheParser diff --git a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/README b/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/README deleted file mode 100644 index c12e149f2..000000000 --- a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/README +++ /dev/null @@ -1,9 +0,0 @@ - -This directory holds Apache 2.0 module-specific configuration files; -any files in this directory which have the ".conf" extension will be -processed as Apache configuration files. - -Files are processed in alphabetical order, so if using configuration -directives which depend on, say, mod_perl being loaded, ensure that -these are placed in a filename later in the sort order than "perl.conf". - diff --git a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/ssl.conf b/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/ssl.conf deleted file mode 100644 index abe07dd0c..000000000 --- a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/ssl.conf +++ /dev/null @@ -1,222 +0,0 @@ -# -# This is the Apache server configuration file providing SSL support. -# It contains the configuration directives to instruct the server how to -# serve pages over an https connection. For detailing information about these -# directives see -# -# Do NOT simply read the instructions in here without understanding -# what they do. They're here only as hints or reminders. If you are unsure -# consult the online docs. You have been warned. -# - -LoadModule ssl_module modules/mod_ssl.so - -# -# When we also provide SSL we have to listen to the -# the HTTPS port in addition. -# -Listen 443 - -## -## SSL Global Context -## -## All SSL configuration in this context applies both to -## the main server and all SSL-enabled virtual hosts. -## - -# Pass Phrase Dialog: -# Configure the pass phrase gathering process. -# The filtering dialog program (`builtin' is an internal -# terminal dialog) has to provide the pass phrase on stdout. -SSLPassPhraseDialog builtin - -# Inter-Process Session Cache: -# Configure the SSL Session Cache: First the mechanism -# to use and second the expiring timeout (in seconds). -SSLSessionCache shmcb:/var/cache/mod_ssl/scache(512000) -SSLSessionCacheTimeout 300 - -# Semaphore: -# Configure the path to the mutual exclusion semaphore the -# SSL engine uses internally for inter-process synchronization. -SSLMutex default - -# Pseudo Random Number Generator (PRNG): -# Configure one or more sources to seed the PRNG of the -# SSL library. The seed data should be of good random quality. -# WARNING! On some platforms /dev/random blocks if not enough entropy -# is available. This means you then cannot use the /dev/random device -# because it would lead to very long connection times (as long as -# it requires to make more entropy available). But usually those -# platforms additionally provide a /dev/urandom device which doesn't -# block. So, if available, use this one instead. Read the mod_ssl User -# Manual for more details. -SSLRandomSeed startup file:/dev/urandom 256 -SSLRandomSeed connect builtin -#SSLRandomSeed startup file:/dev/random 512 -#SSLRandomSeed connect file:/dev/random 512 -#SSLRandomSeed connect file:/dev/urandom 512 - -# -# Use "SSLCryptoDevice" to enable any supported hardware -# accelerators. Use "openssl engine -v" to list supported -# engine names. NOTE: If you enable an accelerator and the -# server does not start, consult the error logs and ensure -# your accelerator is functioning properly. -# -SSLCryptoDevice builtin -#SSLCryptoDevice ubsec - -## -## SSL Virtual Host Context -## - - - -# General setup for the virtual host, inherited from global configuration -#DocumentRoot "/var/www/html" -#ServerName www.example.com:443 - -# Use separate log files for the SSL virtual host; note that LogLevel -# is not inherited from httpd.conf. -ErrorLog logs/ssl_error_log -TransferLog logs/ssl_access_log -LogLevel warn - -# SSL Engine Switch: -# Enable/Disable SSL for this virtual host. -SSLEngine on - -# SSL Protocol support: -# List the enable protocol levels with which clients will be able to -# connect. Disable SSLv2 access by default: -SSLProtocol all -SSLv2 - -# SSL Cipher Suite: -# List the ciphers that the client is permitted to negotiate. -# See the mod_ssl documentation for a complete list. -SSLCipherSuite DEFAULT:!EXP:!SSLv2:!DES:!IDEA:!SEED:+3DES - -# Server Certificate: -# Point SSLCertificateFile at a PEM encoded certificate. If -# the certificate is encrypted, then you will be prompted for a -# pass phrase. Note that a kill -HUP will prompt again. A new -# certificate can be generated using the genkey(1) command. -SSLCertificateFile /etc/pki/tls/certs/localhost.crt - -# Server Private Key: -# If the key is not combined with the certificate, use this -# directive to point at the key file. Keep in mind that if -# you've both a RSA and a DSA private key you can configure -# both in parallel (to also allow the use of DSA ciphers, etc.) -SSLCertificateKeyFile /etc/pki/tls/private/localhost.key - -# Server Certificate Chain: -# Point SSLCertificateChainFile at a file containing the -# concatenation of PEM encoded CA certificates which form the -# certificate chain for the server certificate. Alternatively -# the referenced file can be the same as SSLCertificateFile -# when the CA certificates are directly appended to the server -# certificate for convinience. -#SSLCertificateChainFile /etc/pki/tls/certs/server-chain.crt - -# Certificate Authority (CA): -# Set the CA certificate verification path where to find CA -# certificates for client authentication or alternatively one -# huge file containing all of them (file must be PEM encoded) -#SSLCACertificateFile /etc/pki/tls/certs/ca-bundle.crt - -# Client Authentication (Type): -# Client certificate verification type and depth. Types are -# none, optional, require and optional_no_ca. Depth is a -# number which specifies how deeply to verify the certificate -# issuer chain before deciding the certificate is not valid. -#SSLVerifyClient require -#SSLVerifyDepth 10 - -# Access Control: -# With SSLRequire you can do per-directory access control based -# on arbitrary complex boolean expressions containing server -# variable checks and other lookup directives. The syntax is a -# mixture between C and Perl. See the mod_ssl documentation -# for more details. -# -#SSLRequire ( %{SSL_CIPHER} !~ m/^(EXP|NULL)/ \ -# and %{SSL_CLIENT_S_DN_O} eq "Snake Oil, Ltd." \ -# and %{SSL_CLIENT_S_DN_OU} in {"Staff", "CA", "Dev"} \ -# and %{TIME_WDAY} >= 1 and %{TIME_WDAY} <= 5 \ -# and %{TIME_HOUR} >= 8 and %{TIME_HOUR} <= 20 ) \ -# or %{REMOTE_ADDR} =~ m/^192\.76\.162\.[0-9]+$/ -# - -# SSL Engine Options: -# Set various options for the SSL engine. -# o FakeBasicAuth: -# Translate the client X.509 into a Basic Authorisation. This means that -# the standard Auth/DBMAuth methods can be used for access control. The -# user name is the `one line' version of the client's X.509 certificate. -# Note that no password is obtained from the user. Every entry in the user -# file needs this password: `xxj31ZMTZzkVA'. -# o ExportCertData: -# This exports two additional environment variables: SSL_CLIENT_CERT and -# SSL_SERVER_CERT. These contain the PEM-encoded certificates of the -# server (always existing) and the client (only existing when client -# authentication is used). This can be used to import the certificates -# into CGI scripts. -# o StdEnvVars: -# This exports the standard SSL/TLS related `SSL_*' environment variables. -# Per default this exportation is switched off for performance reasons, -# because the extraction step is an expensive operation and is usually -# useless for serving static content. So one usually enables the -# exportation for CGI and SSI requests only. -# o StrictRequire: -# This denies access when "SSLRequireSSL" or "SSLRequire" applied even -# under a "Satisfy any" situation, i.e. when it applies access is denied -# and no other module can change it. -# o OptRenegotiate: -# This enables optimized SSL connection renegotiation handling when SSL -# directives are used in per-directory context. -#SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire - - SSLOptions +StdEnvVars - - - SSLOptions +StdEnvVars - - -# SSL Protocol Adjustments: -# The safe and default but still SSL/TLS standard compliant shutdown -# approach is that mod_ssl sends the close notify alert but doesn't wait for -# the close notify alert from client. When you need a different shutdown -# approach you can use one of the following variables: -# o ssl-unclean-shutdown: -# This forces an unclean shutdown when the connection is closed, i.e. no -# SSL close notify alert is send or allowed to received. This violates -# the SSL/TLS standard but is needed for some brain-dead browsers. Use -# this when you receive I/O errors because of the standard approach where -# mod_ssl sends the close notify alert. -# o ssl-accurate-shutdown: -# This forces an accurate shutdown when the connection is closed, i.e. a -# SSL close notify alert is send and mod_ssl waits for the close notify -# alert of the client. This is 100% SSL/TLS standard compliant, but in -# practice often causes hanging connections with brain-dead browsers. Use -# this only for browsers where you know that their SSL implementation -# works correctly. -# Notice: Most problems of broken clients are also related to the HTTP -# keep-alive facility, so you usually additionally want to disable -# keep-alive for those clients, too. Use variable "nokeepalive" for this. -# Similarly, one has to force some clients to use HTTP/1.0 to workaround -# their broken HTTP/1.1 implementation. Use variables "downgrade-1.0" and -# "force-response-1.0" for this. -SetEnvIf User-Agent ".*MSIE.*" \ - nokeepalive ssl-unclean-shutdown \ - downgrade-1.0 force-response-1.0 - -# Per-Server Logging: -# The home of a custom SSL log file. Use this when you want a -# compact non-error SSL logfile on a virtual host basis. -CustomLog logs/ssl_request_log \ - "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" - - - diff --git a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/test.example.com.conf b/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/test.example.com.conf deleted file mode 100644 index 3dd7b18f1..000000000 --- a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/test.example.com.conf +++ /dev/null @@ -1,7 +0,0 @@ - - ServerName test.example.com - ServerAdmin webmaster@dummy-host.example.com - DocumentRoot /var/www/htdocs - ErrorLog logs/dummy-host.example.com-error_log - CustomLog logs/dummy-host.example.com-access_log common - diff --git a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/welcome.conf b/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/welcome.conf deleted file mode 100644 index c1d23c512..000000000 --- a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/welcome.conf +++ /dev/null @@ -1,11 +0,0 @@ -# -# This configuration file enables the default "Welcome" -# page if there is no default index page present for -# the root URL. To disable the Welcome page, comment -# out all the lines below. -# - - Options -Indexes - ErrorDocument 403 /error/noindex.html - - diff --git a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf/httpd.conf b/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf/httpd.conf deleted file mode 100644 index eac6143da..000000000 --- a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf/httpd.conf +++ /dev/null @@ -1,1009 +0,0 @@ -# -# This is the main Apache server configuration file. It contains the -# configuration directives that give the server its instructions. -# See for detailed information. -# In particular, see -# -# for a discussion of each configuration directive. -# -# -# Do NOT simply read the instructions in here without understanding -# what they do. They're here only as hints or reminders. If you are unsure -# consult the online docs. You have been warned. -# -# The configuration directives are grouped into three basic sections: -# 1. Directives that control the operation of the Apache server process as a -# whole (the 'global environment'). -# 2. Directives that define the parameters of the 'main' or 'default' server, -# which responds to requests that aren't handled by a virtual host. -# These directives also provide default values for the settings -# of all virtual hosts. -# 3. Settings for virtual hosts, which allow Web requests to be sent to -# different IP addresses or hostnames and have them handled by the -# same Apache server process. -# -# Configuration and logfile names: If the filenames you specify for many -# of the server's control files begin with "/" (or "drive:/" for Win32), the -# server will use that explicit path. If the filenames do *not* begin -# with "/", the value of ServerRoot is prepended -- so "logs/foo.log" -# with ServerRoot set to "/etc/httpd" will be interpreted by the -# server as "/etc/httpd/logs/foo.log". -# - -### Section 1: Global Environment -# -# The directives in this section affect the overall operation of Apache, -# such as the number of concurrent requests it can handle or where it -# can find its configuration files. -# - -# -# Don't give away too much information about all the subcomponents -# we are running. Comment out this line if you don't mind remote sites -# finding out what major optional modules you are running -ServerTokens OS - -# -# ServerRoot: The top of the directory tree under which the server's -# configuration, error, and log files are kept. -# -# NOTE! If you intend to place this on an NFS (or otherwise network) -# mounted filesystem then please read the LockFile documentation -# (available at ); -# you will save yourself a lot of trouble. -# -# Do NOT add a slash at the end of the directory path. -# -ServerRoot "/etc/httpd" - -# -# PidFile: The file in which the server should record its process -# identification number when it starts. Note the PIDFILE variable in -# /etc/sysconfig/httpd must be set appropriately if this location is -# changed. -# -PidFile run/httpd.pid - -# -# Timeout: The number of seconds before receives and sends time out. -# -Timeout 60 - -# -# KeepAlive: Whether or not to allow persistent connections (more than -# one request per connection). Set to "Off" to deactivate. -# -KeepAlive Off - -# -# MaxKeepAliveRequests: The maximum number of requests to allow -# during a persistent connection. Set to 0 to allow an unlimited amount. -# We recommend you leave this number high, for maximum performance. -# -MaxKeepAliveRequests 100 - -# -# KeepAliveTimeout: Number of seconds to wait for the next request from the -# same client on the same connection. -# -KeepAliveTimeout 15 - -## -## Server-Pool Size Regulation (MPM specific) -## - -# prefork MPM -# StartServers: number of server processes to start -# MinSpareServers: minimum number of server processes which are kept spare -# MaxSpareServers: maximum number of server processes which are kept spare -# ServerLimit: maximum value for MaxClients for the lifetime of the server -# MaxClients: maximum number of server processes allowed to start -# MaxRequestsPerChild: maximum number of requests a server process serves - -StartServers 8 -MinSpareServers 5 -MaxSpareServers 20 -ServerLimit 256 -MaxClients 256 -MaxRequestsPerChild 4000 - - -# worker MPM -# StartServers: initial number of server processes to start -# MaxClients: maximum number of simultaneous client connections -# MinSpareThreads: minimum number of worker threads which are kept spare -# MaxSpareThreads: maximum number of worker threads which are kept spare -# ThreadsPerChild: constant number of worker threads in each server process -# MaxRequestsPerChild: maximum number of requests a server process serves - -StartServers 4 -MaxClients 300 -MinSpareThreads 25 -MaxSpareThreads 75 -ThreadsPerChild 25 -MaxRequestsPerChild 0 - - -# -# Listen: Allows you to bind Apache to specific IP addresses and/or -# ports, in addition to the default. See also the -# directive. -# -# Change this to Listen on specific IP addresses as shown below to -# prevent Apache from glomming onto all bound IP addresses (0.0.0.0) -# -#Listen 12.34.56.78:80 -Listen 80 - -# -# Dynamic Shared Object (DSO) Support -# -# To be able to use the functionality of a module which was built as a DSO you -# have to place corresponding `LoadModule' lines at this location so the -# directives contained in it are actually available _before_ they are used. -# Statically compiled modules (those listed by `httpd -l') do not need -# to be loaded here. -# -# Example: -# LoadModule foo_module modules/mod_foo.so -# -LoadModule auth_basic_module modules/mod_auth_basic.so -LoadModule auth_digest_module modules/mod_auth_digest.so -LoadModule authn_file_module modules/mod_authn_file.so -LoadModule authn_alias_module modules/mod_authn_alias.so -LoadModule authn_anon_module modules/mod_authn_anon.so -LoadModule authn_dbm_module modules/mod_authn_dbm.so -LoadModule authn_default_module modules/mod_authn_default.so -LoadModule authz_host_module modules/mod_authz_host.so -LoadModule authz_user_module modules/mod_authz_user.so -LoadModule authz_owner_module modules/mod_authz_owner.so -LoadModule authz_groupfile_module modules/mod_authz_groupfile.so -LoadModule authz_dbm_module modules/mod_authz_dbm.so -LoadModule authz_default_module modules/mod_authz_default.so -LoadModule ldap_module modules/mod_ldap.so -LoadModule authnz_ldap_module modules/mod_authnz_ldap.so -LoadModule include_module modules/mod_include.so -LoadModule log_config_module modules/mod_log_config.so -LoadModule logio_module modules/mod_logio.so -LoadModule env_module modules/mod_env.so -LoadModule ext_filter_module modules/mod_ext_filter.so -LoadModule mime_magic_module modules/mod_mime_magic.so -LoadModule expires_module modules/mod_expires.so -LoadModule deflate_module modules/mod_deflate.so -LoadModule headers_module modules/mod_headers.so -LoadModule usertrack_module modules/mod_usertrack.so -LoadModule setenvif_module modules/mod_setenvif.so -LoadModule mime_module modules/mod_mime.so -LoadModule dav_module modules/mod_dav.so -LoadModule status_module modules/mod_status.so -LoadModule autoindex_module modules/mod_autoindex.so -LoadModule info_module modules/mod_info.so -LoadModule dav_fs_module modules/mod_dav_fs.so -LoadModule vhost_alias_module modules/mod_vhost_alias.so -LoadModule negotiation_module modules/mod_negotiation.so -LoadModule dir_module modules/mod_dir.so -LoadModule actions_module modules/mod_actions.so -LoadModule speling_module modules/mod_speling.so -LoadModule userdir_module modules/mod_userdir.so -LoadModule alias_module modules/mod_alias.so -LoadModule substitute_module modules/mod_substitute.so -LoadModule rewrite_module modules/mod_rewrite.so -LoadModule proxy_module modules/mod_proxy.so -LoadModule proxy_balancer_module modules/mod_proxy_balancer.so -LoadModule proxy_ftp_module modules/mod_proxy_ftp.so -LoadModule proxy_http_module modules/mod_proxy_http.so -LoadModule proxy_ajp_module modules/mod_proxy_ajp.so -LoadModule proxy_connect_module modules/mod_proxy_connect.so -LoadModule cache_module modules/mod_cache.so -LoadModule suexec_module modules/mod_suexec.so -LoadModule disk_cache_module modules/mod_disk_cache.so -LoadModule cgi_module modules/mod_cgi.so -LoadModule version_module modules/mod_version.so - -# -# The following modules are not loaded by default: -# -#LoadModule asis_module modules/mod_asis.so -#LoadModule authn_dbd_module modules/mod_authn_dbd.so -#LoadModule cern_meta_module modules/mod_cern_meta.so -#LoadModule cgid_module modules/mod_cgid.so -#LoadModule dbd_module modules/mod_dbd.so -#LoadModule dumpio_module modules/mod_dumpio.so -#LoadModule filter_module modules/mod_filter.so -#LoadModule ident_module modules/mod_ident.so -#LoadModule log_forensic_module modules/mod_log_forensic.so -#LoadModule unique_id_module modules/mod_unique_id.so -# - -# -# Load config files from the config directory "/etc/httpd/conf.d". -# -Include conf.d/*.conf - -# -# ExtendedStatus controls whether Apache will generate "full" status -# information (ExtendedStatus On) or just basic information (ExtendedStatus -# Off) when the "server-status" handler is called. The default is Off. -# -#ExtendedStatus On - -# -# If you wish httpd to run as a different user or group, you must run -# httpd as root initially and it will switch. -# -# User/Group: The name (or #number) of the user/group to run httpd as. -# . On SCO (ODT 3) use "User nouser" and "Group nogroup". -# . On HPUX you may not be able to use shared memory as nobody, and the -# suggested workaround is to create a user www and use that user. -# NOTE that some kernels refuse to setgid(Group) or semctl(IPC_SET) -# when the value of (unsigned)Group is above 60000; -# don't use Group #-1 on these systems! -# -User apache -Group apache - -### Section 2: 'Main' server configuration -# -# The directives in this section set up the values used by the 'main' -# server, which responds to any requests that aren't handled by a -# definition. These values also provide defaults for -# any containers you may define later in the file. -# -# All of these directives may appear inside containers, -# in which case these default settings will be overridden for the -# virtual host being defined. -# - -# -# ServerAdmin: Your address, where problems with the server should be -# e-mailed. This address appears on some server-generated pages, such -# as error documents. e.g. admin@your-domain.com -# -ServerAdmin root@localhost - -# -# ServerName gives the name and port that the server uses to identify itself. -# This can often be determined automatically, but we recommend you specify -# it explicitly to prevent problems during startup. -# -# If this is not set to valid DNS name for your host, server-generated -# redirections will not work. See also the UseCanonicalName directive. -# -# If your host doesn't have a registered DNS name, enter its IP address here. -# You will have to access it by its address anyway, and this will make -# redirections work in a sensible way. -# -#ServerName www.example.com:80 - -# -# UseCanonicalName: Determines how Apache constructs self-referencing -# URLs and the SERVER_NAME and SERVER_PORT variables. -# When set "Off", Apache will use the Hostname and Port supplied -# by the client. When set "On", Apache will use the value of the -# ServerName directive. -# -UseCanonicalName Off - -# -# DocumentRoot: The directory out of which you will serve your -# documents. By default, all requests are taken from this directory, but -# symbolic links and aliases may be used to point to other locations. -# -DocumentRoot "/var/www/html" - -# -# Each directory to which Apache has access can be configured with respect -# to which services and features are allowed and/or disabled in that -# directory (and its subdirectories). -# -# First, we configure the "default" to be a very restrictive set of -# features. -# - - Options FollowSymLinks - AllowOverride None - - -# -# Note that from this point forward you must specifically allow -# particular features to be enabled - so if something's not working as -# you might expect, make sure that you have specifically enabled it -# below. -# - -# -# This should be changed to whatever you set DocumentRoot to. -# - - -# -# Possible values for the Options directive are "None", "All", -# or any combination of: -# Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews -# -# Note that "MultiViews" must be named *explicitly* --- "Options All" -# doesn't give it to you. -# -# The Options directive is both complicated and important. Please see -# http://httpd.apache.org/docs/2.2/mod/core.html#options -# for more information. -# - Options Indexes FollowSymLinks - -# -# AllowOverride controls what directives may be placed in .htaccess files. -# It can be "All", "None", or any combination of the keywords: -# Options FileInfo AuthConfig Limit -# - AllowOverride None - -# -# Controls who can get stuff from this server. -# - Order allow,deny - Allow from all - - - -# -# UserDir: The name of the directory that is appended onto a user's home -# directory if a ~user request is received. -# -# The path to the end user account 'public_html' directory must be -# accessible to the webserver userid. This usually means that ~userid -# must have permissions of 711, ~userid/public_html must have permissions -# of 755, and documents contained therein must be world-readable. -# Otherwise, the client will only receive a "403 Forbidden" message. -# -# See also: http://httpd.apache.org/docs/misc/FAQ.html#forbidden -# - - # - # UserDir is disabled by default since it can confirm the presence - # of a username on the system (depending on home directory - # permissions). - # - UserDir disabled - - # - # To enable requests to /~user/ to serve the user's public_html - # directory, remove the "UserDir disabled" line above, and uncomment - # the following line instead: - # - #UserDir public_html - - - -# -# Control access to UserDir directories. The following is an example -# for a site where these directories are restricted to read-only. -# -# -# AllowOverride FileInfo AuthConfig Limit -# Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec -# -# Order allow,deny -# Allow from all -# -# -# Order deny,allow -# Deny from all -# -# - -# -# DirectoryIndex: sets the file that Apache will serve if a directory -# is requested. -# -# The index.html.var file (a type-map) is used to deliver content- -# negotiated documents. The MultiViews Option can be used for the -# same purpose, but it is much slower. -# -DirectoryIndex index.html index.html.var - -# -# AccessFileName: The name of the file to look for in each directory -# for additional configuration directives. See also the AllowOverride -# directive. -# -AccessFileName .htaccess - -# -# The following lines prevent .htaccess and .htpasswd files from being -# viewed by Web clients. -# - - Order allow,deny - Deny from all - Satisfy All - - -# -# TypesConfig describes where the mime.types file (or equivalent) is -# to be found. -# -TypesConfig /etc/mime.types - -# -# DefaultType is the default MIME type the server will use for a document -# if it cannot otherwise determine one, such as from filename extensions. -# If your server contains mostly text or HTML documents, "text/plain" is -# a good value. If most of your content is binary, such as applications -# or images, you may want to use "application/octet-stream" instead to -# keep browsers from trying to display binary files as though they are -# text. -# -DefaultType text/plain - -# -# The mod_mime_magic module allows the server to use various hints from the -# contents of the file itself to determine its type. The MIMEMagicFile -# directive tells the module where the hint definitions are located. -# - -# MIMEMagicFile /usr/share/magic.mime - MIMEMagicFile conf/magic - - -# -# HostnameLookups: Log the names of clients or just their IP addresses -# e.g., www.apache.org (on) or 204.62.129.132 (off). -# The default is off because it'd be overall better for the net if people -# had to knowingly turn this feature on, since enabling it means that -# each client request will result in AT LEAST one lookup request to the -# nameserver. -# -HostnameLookups Off - -# -# EnableMMAP: Control whether memory-mapping is used to deliver -# files (assuming that the underlying OS supports it). -# The default is on; turn this off if you serve from NFS-mounted -# filesystems. On some systems, turning it off (regardless of -# filesystem) can improve performance; for details, please see -# http://httpd.apache.org/docs/2.2/mod/core.html#enablemmap -# -#EnableMMAP off - -# -# EnableSendfile: Control whether the sendfile kernel support is -# used to deliver files (assuming that the OS supports it). -# The default is on; turn this off if you serve from NFS-mounted -# filesystems. Please see -# http://httpd.apache.org/docs/2.2/mod/core.html#enablesendfile -# -#EnableSendfile off - -# -# ErrorLog: The location of the error log file. -# If you do not specify an ErrorLog directive within a -# container, error messages relating to that virtual host will be -# logged here. If you *do* define an error logfile for a -# container, that host's errors will be logged there and not here. -# -ErrorLog logs/error_log - -# -# LogLevel: Control the number of messages logged to the error_log. -# Possible values include: debug, info, notice, warn, error, crit, -# alert, emerg. -# -LogLevel warn - -# -# The following directives define some format nicknames for use with -# a CustomLog directive (see below). -# -LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined -LogFormat "%h %l %u %t \"%r\" %>s %b" common -LogFormat "%{Referer}i -> %U" referer -LogFormat "%{User-agent}i" agent - -# "combinedio" includes actual counts of actual bytes received (%I) and sent (%O); this -# requires the mod_logio module to be loaded. -#LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio - -# -# The location and format of the access logfile (Common Logfile Format). -# If you do not define any access logfiles within a -# container, they will be logged here. Contrariwise, if you *do* -# define per- access logfiles, transactions will be -# logged therein and *not* in this file. -# -#CustomLog logs/access_log common - -# -# If you would like to have separate agent and referer logfiles, uncomment -# the following directives. -# -#CustomLog logs/referer_log referer -#CustomLog logs/agent_log agent - -# -# For a single logfile with access, agent, and referer information -# (Combined Logfile Format), use the following directive: -# -CustomLog logs/access_log combined - -# -# Optionally add a line containing the server version and virtual host -# name to server-generated pages (internal error documents, FTP directory -# listings, mod_status and mod_info output etc., but not CGI generated -# documents or custom error documents). -# Set to "EMail" to also include a mailto: link to the ServerAdmin. -# Set to one of: On | Off | EMail -# -ServerSignature On - -# -# Aliases: Add here as many aliases as you need (with no limit). The format is -# Alias fakename realname -# -# Note that if you include a trailing / on fakename then the server will -# require it to be present in the URL. So "/icons" isn't aliased in this -# example, only "/icons/". If the fakename is slash-terminated, then the -# realname must also be slash terminated, and if the fakename omits the -# trailing slash, the realname must also omit it. -# -# We include the /icons/ alias for FancyIndexed directory listings. If you -# do not use FancyIndexing, you may comment this out. -# -Alias /icons/ "/var/www/icons/" - - - Options Indexes MultiViews FollowSymLinks - AllowOverride None - Order allow,deny - Allow from all - - -# -# WebDAV module configuration section. -# - - # Location of the WebDAV lock database. - DAVLockDB /var/lib/dav/lockdb - - -# -# ScriptAlias: This controls which directories contain server scripts. -# ScriptAliases are essentially the same as Aliases, except that -# documents in the realname directory are treated as applications and -# run by the server when requested rather than as documents sent to the client. -# The same rules about trailing "/" apply to ScriptAlias directives as to -# Alias. -# -ScriptAlias /cgi-bin/ "/var/www/cgi-bin/" - -# -# "/var/www/cgi-bin" should be changed to whatever your ScriptAliased -# CGI directory exists, if you have that configured. -# - - AllowOverride None - Options None - Order allow,deny - Allow from all - - -# -# Redirect allows you to tell clients about documents which used to exist in -# your server's namespace, but do not anymore. This allows you to tell the -# clients where to look for the relocated document. -# Example: -# Redirect permanent /foo http://www.example.com/bar - -# -# Directives controlling the display of server-generated directory listings. -# - -# -# IndexOptions: Controls the appearance of server-generated directory -# listings. -# -IndexOptions FancyIndexing VersionSort NameWidth=* HTMLTable Charset=UTF-8 - -# -# AddIcon* directives tell the server which icon to show for different -# files or filename extensions. These are only displayed for -# FancyIndexed directories. -# -AddIconByEncoding (CMP,/icons/compressed.gif) x-compress x-gzip - -AddIconByType (TXT,/icons/text.gif) text/* -AddIconByType (IMG,/icons/image2.gif) image/* -AddIconByType (SND,/icons/sound2.gif) audio/* -AddIconByType (VID,/icons/movie.gif) video/* - -AddIcon /icons/binary.gif .bin .exe -AddIcon /icons/binhex.gif .hqx -AddIcon /icons/tar.gif .tar -AddIcon /icons/world2.gif .wrl .wrl.gz .vrml .vrm .iv -AddIcon /icons/compressed.gif .Z .z .tgz .gz .zip -AddIcon /icons/a.gif .ps .ai .eps -AddIcon /icons/layout.gif .html .shtml .htm .pdf -AddIcon /icons/text.gif .txt -AddIcon /icons/c.gif .c -AddIcon /icons/p.gif .pl .py -AddIcon /icons/f.gif .for -AddIcon /icons/dvi.gif .dvi -AddIcon /icons/uuencoded.gif .uu -AddIcon /icons/script.gif .conf .sh .shar .csh .ksh .tcl -AddIcon /icons/tex.gif .tex -AddIcon /icons/bomb.gif /core - -AddIcon /icons/back.gif .. -AddIcon /icons/hand.right.gif README -AddIcon /icons/folder.gif ^^DIRECTORY^^ -AddIcon /icons/blank.gif ^^BLANKICON^^ - -# -# DefaultIcon is which icon to show for files which do not have an icon -# explicitly set. -# -DefaultIcon /icons/unknown.gif - -# -# AddDescription allows you to place a short description after a file in -# server-generated indexes. These are only displayed for FancyIndexed -# directories. -# Format: AddDescription "description" filename -# -#AddDescription "GZIP compressed document" .gz -#AddDescription "tar archive" .tar -#AddDescription "GZIP compressed tar archive" .tgz - -# -# ReadmeName is the name of the README file the server will look for by -# default, and append to directory listings. -# -# HeaderName is the name of a file which should be prepended to -# directory indexes. -ReadmeName README.html -HeaderName HEADER.html - -# -# IndexIgnore is a set of filenames which directory indexing should ignore -# and not include in the listing. Shell-style wildcarding is permitted. -# -IndexIgnore .??* *~ *# HEADER* README* RCS CVS *,v *,t - -# -# DefaultLanguage and AddLanguage allows you to specify the language of -# a document. You can then use content negotiation to give a browser a -# file in a language the user can understand. -# -# Specify a default language. This means that all data -# going out without a specific language tag (see below) will -# be marked with this one. You probably do NOT want to set -# this unless you are sure it is correct for all cases. -# -# * It is generally better to not mark a page as -# * being a certain language than marking it with the wrong -# * language! -# -# DefaultLanguage nl -# -# Note 1: The suffix does not have to be the same as the language -# keyword --- those with documents in Polish (whose net-standard -# language code is pl) may wish to use "AddLanguage pl .po" to -# avoid the ambiguity with the common suffix for perl scripts. -# -# Note 2: The example entries below illustrate that in some cases -# the two character 'Language' abbreviation is not identical to -# the two character 'Country' code for its country, -# E.g. 'Danmark/dk' versus 'Danish/da'. -# -# Note 3: In the case of 'ltz' we violate the RFC by using a three char -# specifier. There is 'work in progress' to fix this and get -# the reference data for rfc1766 cleaned up. -# -# Catalan (ca) - Croatian (hr) - Czech (cs) - Danish (da) - Dutch (nl) -# English (en) - Esperanto (eo) - Estonian (et) - French (fr) - German (de) -# Greek-Modern (el) - Hebrew (he) - Italian (it) - Japanese (ja) -# Korean (ko) - Luxembourgeois* (ltz) - Norwegian Nynorsk (nn) -# Norwegian (no) - Polish (pl) - Portuguese (pt) -# Brazilian Portuguese (pt-BR) - Russian (ru) - Swedish (sv) -# Simplified Chinese (zh-CN) - Spanish (es) - Traditional Chinese (zh-TW) -# -AddLanguage ca .ca -AddLanguage cs .cz .cs -AddLanguage da .dk -AddLanguage de .de -AddLanguage el .el -AddLanguage en .en -AddLanguage eo .eo -AddLanguage es .es -AddLanguage et .et -AddLanguage fr .fr -AddLanguage he .he -AddLanguage hr .hr -AddLanguage it .it -AddLanguage ja .ja -AddLanguage ko .ko -AddLanguage ltz .ltz -AddLanguage nl .nl -AddLanguage nn .nn -AddLanguage no .no -AddLanguage pl .po -AddLanguage pt .pt -AddLanguage pt-BR .pt-br -AddLanguage ru .ru -AddLanguage sv .sv -AddLanguage zh-CN .zh-cn -AddLanguage zh-TW .zh-tw - -# -# LanguagePriority allows you to give precedence to some languages -# in case of a tie during content negotiation. -# -# Just list the languages in decreasing order of preference. We have -# more or less alphabetized them here. You probably want to change this. -# -LanguagePriority en ca cs da de el eo es et fr he hr it ja ko ltz nl nn no pl pt pt-BR ru sv zh-CN zh-TW - -# -# ForceLanguagePriority allows you to serve a result page rather than -# MULTIPLE CHOICES (Prefer) [in case of a tie] or NOT ACCEPTABLE (Fallback) -# [in case no accepted languages matched the available variants] -# -ForceLanguagePriority Prefer Fallback - -# -# Specify a default charset for all content served; this enables -# interpretation of all content as UTF-8 by default. To use the -# default browser choice (ISO-8859-1), or to allow the META tags -# in HTML content to override this choice, comment out this -# directive: -# -AddDefaultCharset UTF-8 - -# -# AddType allows you to add to or override the MIME configuration -# file mime.types for specific file types. -# -#AddType application/x-tar .tgz - -# -# AddEncoding allows you to have certain browsers uncompress -# information on the fly. Note: Not all browsers support this. -# Despite the name similarity, the following Add* directives have nothing -# to do with the FancyIndexing customization directives above. -# -#AddEncoding x-compress .Z -#AddEncoding x-gzip .gz .tgz - -# If the AddEncoding directives above are commented-out, then you -# probably should define those extensions to indicate media types: -# -AddType application/x-compress .Z -AddType application/x-gzip .gz .tgz - -# -# MIME-types for downloading Certificates and CRLs -# -AddType application/x-x509-ca-cert .crt -AddType application/x-pkcs7-crl .crl - -# -# AddHandler allows you to map certain file extensions to "handlers": -# actions unrelated to filetype. These can be either built into the server -# or added with the Action directive (see below) -# -# To use CGI scripts outside of ScriptAliased directories: -# (You will also need to add "ExecCGI" to the "Options" directive.) -# -#AddHandler cgi-script .cgi - -# -# For files that include their own HTTP headers: -# -#AddHandler send-as-is asis - -# -# For type maps (negotiated resources): -# (This is enabled by default to allow the Apache "It Worked" page -# to be distributed in multiple languages.) -# -AddHandler type-map var - -# -# Filters allow you to process content before it is sent to the client. -# -# To parse .shtml files for server-side includes (SSI): -# (You will also need to add "Includes" to the "Options" directive.) -# -AddType text/html .shtml -AddOutputFilter INCLUDES .shtml - -# -# Action lets you define media types that will execute a script whenever -# a matching file is called. This eliminates the need for repeated URL -# pathnames for oft-used CGI file processors. -# Format: Action media/type /cgi-script/location -# Format: Action handler-name /cgi-script/location -# - -# -# Customizable error responses come in three flavors: -# 1) plain text 2) local redirects 3) external redirects -# -# Some examples: -#ErrorDocument 500 "The server made a boo boo." -#ErrorDocument 404 /missing.html -#ErrorDocument 404 "/cgi-bin/missing_handler.pl" -#ErrorDocument 402 http://www.example.com/subscription_info.html -# - -# -# Putting this all together, we can internationalize error responses. -# -# We use Alias to redirect any /error/HTTP_.html.var response to -# our collection of by-error message multi-language collections. We use -# includes to substitute the appropriate text. -# -# You can modify the messages' appearance without changing any of the -# default HTTP_.html.var files by adding the line: -# -# Alias /error/include/ "/your/include/path/" -# -# which allows you to create your own set of files by starting with the -# /var/www/error/include/ files and -# copying them to /your/include/path/, even on a per-VirtualHost basis. -# - -Alias /error/ "/var/www/error/" - - - - - AllowOverride None - Options IncludesNoExec - AddOutputFilter Includes html - AddHandler type-map var - Order allow,deny - Allow from all - LanguagePriority en es de fr - ForceLanguagePriority Prefer Fallback - - -# ErrorDocument 400 /error/HTTP_BAD_REQUEST.html.var -# ErrorDocument 401 /error/HTTP_UNAUTHORIZED.html.var -# ErrorDocument 403 /error/HTTP_FORBIDDEN.html.var -# ErrorDocument 404 /error/HTTP_NOT_FOUND.html.var -# ErrorDocument 405 /error/HTTP_METHOD_NOT_ALLOWED.html.var -# ErrorDocument 408 /error/HTTP_REQUEST_TIME_OUT.html.var -# ErrorDocument 410 /error/HTTP_GONE.html.var -# ErrorDocument 411 /error/HTTP_LENGTH_REQUIRED.html.var -# ErrorDocument 412 /error/HTTP_PRECONDITION_FAILED.html.var -# ErrorDocument 413 /error/HTTP_REQUEST_ENTITY_TOO_LARGE.html.var -# ErrorDocument 414 /error/HTTP_REQUEST_URI_TOO_LARGE.html.var -# ErrorDocument 415 /error/HTTP_UNSUPPORTED_MEDIA_TYPE.html.var -# ErrorDocument 500 /error/HTTP_INTERNAL_SERVER_ERROR.html.var -# ErrorDocument 501 /error/HTTP_NOT_IMPLEMENTED.html.var -# ErrorDocument 502 /error/HTTP_BAD_GATEWAY.html.var -# ErrorDocument 503 /error/HTTP_SERVICE_UNAVAILABLE.html.var -# ErrorDocument 506 /error/HTTP_VARIANT_ALSO_VARIES.html.var - - - - -# -# The following directives modify normal HTTP response behavior to -# handle known problems with browser implementations. -# -BrowserMatch "Mozilla/2" nokeepalive -BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0 -BrowserMatch "RealPlayer 4\.0" force-response-1.0 -BrowserMatch "Java/1\.0" force-response-1.0 -BrowserMatch "JDK/1\.0" force-response-1.0 - -# -# The following directive disables redirects on non-GET requests for -# a directory that does not include the trailing slash. This fixes a -# problem with Microsoft WebFolders which does not appropriately handle -# redirects for folders with DAV methods. -# Same deal with Apple's DAV filesystem and Gnome VFS support for DAV. -# -BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully -BrowserMatch "MS FrontPage" redirect-carefully -BrowserMatch "^WebDrive" redirect-carefully -BrowserMatch "^WebDAVFS/1.[0123]" redirect-carefully -BrowserMatch "^gnome-vfs/1.0" redirect-carefully -BrowserMatch "^XML Spy" redirect-carefully -BrowserMatch "^Dreamweaver-WebDAV-SCM1" redirect-carefully - -# -# Allow server status reports generated by mod_status, -# with the URL of http://servername/server-status -# Change the ".example.com" to match your domain to enable. -# -# -# SetHandler server-status -# Order deny,allow -# Deny from all -# Allow from .example.com -# - -# -# Allow remote server configuration reports, with the URL of -# http://servername/server-info (requires that mod_info.c be loaded). -# Change the ".example.com" to match your domain to enable. -# -# -# SetHandler server-info -# Order deny,allow -# Deny from all -# Allow from .example.com -# - -# -# Proxy Server directives. Uncomment the following lines to -# enable the proxy server: -# -# -#ProxyRequests On -# -# -# Order deny,allow -# Deny from all -# Allow from .example.com -# - -# -# Enable/disable the handling of HTTP/1.1 "Via:" headers. -# ("Full" adds the server version; "Block" removes all outgoing Via: headers) -# Set to one of: Off | On | Full | Block -# -#ProxyVia On - -# -# To enable a cache of proxied content, uncomment the following lines. -# See http://httpd.apache.org/docs/2.2/mod/mod_cache.html for more details. -# -# -# CacheEnable disk / -# CacheRoot "/var/cache/mod_proxy" -# -# - -# -# End of proxy directives. - -### Section 3: Virtual Hosts -# -# VirtualHost: If you want to maintain multiple domains/hostnames on your -# machine you can setup VirtualHost containers for them. Most configurations -# use only name-based virtual hosts so the server doesn't need to worry about -# IP addresses. This is indicated by the asterisks in the directives below. -# -# Please see the documentation at -# -# for further details before you try to setup virtual hosts. -# -# You may use the command line option '-S' to verify your virtual host -# configuration. - -# -# Use name-based virtual hosting. -# -#NameVirtualHost *:80 -# -# NOTE: NameVirtualHost cannot be used without a port specifier -# (e.g. :80) if mod_ssl is being used, due to the nature of the -# SSL protocol. -# - -# -# VirtualHost example: -# Almost any Apache directive may go into a VirtualHost container. -# The first VirtualHost section is used for requests without a known -# server name. -# -# -# ServerAdmin webmaster@dummy-host.example.com -# DocumentRoot /www/docs/dummy-host.example.com -# ServerName dummy-host.example.com -# ErrorLog logs/dummy-host.example.com-error_log -# CustomLog logs/dummy-host.example.com-access_log common -# From 012314d946b4e2bd9b60053166f42ff9617ffba6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2022 17:28:47 -0700 Subject: [PATCH 13/29] Deprecate source address (#9389) * deprecate source_address * filter warnings * fix route53 tests * test warning * update docstring --- acme/acme/client.py | 13 +++++++++++-- acme/tests/client_test.py | 5 ++++- certbot/CHANGELOG.md | 2 ++ pytest.ini | 10 +++------- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index e1dc9040f..dbb2a23f1 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -32,7 +32,13 @@ import OpenSSL import requests from requests.adapters import HTTPAdapter from requests.utils import parse_header_links -from requests_toolbelt.adapters.source import SourceAddressAdapter +# We're capturing the warnings described at +# https://github.com/requests/toolbelt/issues/331 until we can remove this +# dependency in Certbot 2.0. +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "'urllib3.contrib.pyopenssl", + DeprecationWarning) + from requests_toolbelt.adapters.source import SourceAddressAdapter from acme import challenges from acme import crypto_util @@ -1031,7 +1037,8 @@ class ClientNetwork: :param bool verify_ssl: Whether to verify certificates on SSL connections. :param str user_agent: String to send as User-Agent header. :param float timeout: Timeout for requests. - :param source_address: Optional source address to bind to when making requests. + :param source_address: Optional source address to bind to when making + requests. (deprecated since 1.30.0) :type source_address: str or tuple(str, int) """ def __init__(self, key: jose.JWK, account: Optional[messages.RegistrationResource] = None, @@ -1049,6 +1056,8 @@ class ClientNetwork: adapter = HTTPAdapter() if source_address is not None: + warnings.warn("Support for source_address is deprecated and will be " + "removed soon.", DeprecationWarning, stacklevel=2) adapter = SourceAddressAdapter(source_address) self.session.mount("http://", adapter) diff --git a/acme/tests/client_test.py b/acme/tests/client_test.py index 7ce28b4fe..1d9aa27fe 100644 --- a/acme/tests/client_test.py +++ b/acme/tests/client_test.py @@ -1343,7 +1343,10 @@ class ClientNetworkSourceAddressBindingTest(unittest.TestCase): def test_source_address_set(self): from acme.client import ClientNetwork - net = ClientNetwork(key=None, alg=None, source_address=self.source_address) + with mock.patch('warnings.warn') as mock_warn: + net = ClientNetwork(key=None, alg=None, source_address=self.source_address) + mock_warn.assert_called_once() + self.assertIn('source_address', mock_warn.call_args[0][0]) for adapter in net.session.adapters.values(): self.assertIn(self.source_address, adapter.source_address) diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 8a46f71ba..90df55e53 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -12,6 +12,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). * The `certbot-dns-cloudxns` plugin is now deprecated and will be removed in the next major release of Certbot. +* The `source_address` argument for `acme.client.ClientNetwork` is deprecated + and support for it will be removed in the next major release. ### Fixed diff --git a/pytest.ini b/pytest.ini index 92a403451..9e8fb5c7b 100644 --- a/pytest.ini +++ b/pytest.ini @@ -22,11 +22,8 @@ # the certbot.display.util module. # 5) A deprecation warning is raised in dnspython==1.15.0 in the oldest tests for # certbot-dns-rfc2136. -# 6) The vendored version of six in botocore causes ImportWarnings in Python -# 3.10+. See https://github.com/boto/botocore/issues/2548. -# 7) botocore's default TLS settings raise deprecation warnings in Python -# 3.10+, but their values are sane from a security perspective. See -# https://github.com/boto/botocore/issues/2550. +# 6) botocore is currently using deprecated urllib3 functionality. See +# https://github.com/boto/botocore/issues/2744. filterwarnings = error ignore:The external mock module:PendingDeprecationWarning @@ -34,5 +31,4 @@ filterwarnings = ignore:.*attribute in certbot.interfaces module is deprecated:DeprecationWarning ignore:.*attribute in certbot.display.util module is deprecated:DeprecationWarning ignore:decodestring\(\) is a deprecated alias:DeprecationWarning:dns - ignore:_SixMetaPathImporter.:ImportWarning - ignore:ssl.PROTOCOL_TLS:DeprecationWarning:botocore + ignore:'urllib3.contrib.pyopenssl:DeprecationWarning:botocore From f9d148be56929813af739670474e489db0936a56 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 29 Aug 2022 23:39:48 -0700 Subject: [PATCH 14/29] Upgrade CI OS (#9391) * upgrade ubuntu * upgrade macos * use python3 --- .../templates/jobs/extended-tests-jobs.yml | 6 ++--- .../templates/jobs/packaging-jobs.yml | 10 ++++---- .../templates/jobs/standard-tests-jobs.yml | 24 +++++++++---------- .../templates/stages/deploy-stage.yml | 4 ++-- .../templates/steps/tox-steps.yml | 8 +++---- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.azure-pipelines/templates/jobs/extended-tests-jobs.yml b/.azure-pipelines/templates/jobs/extended-tests-jobs.yml index c5a1fb628..7c586ee5b 100644 --- a/.azure-pipelines/templates/jobs/extended-tests-jobs.yml +++ b/.azure-pipelines/templates/jobs/extended-tests-jobs.yml @@ -2,7 +2,7 @@ jobs: - job: extended_test variables: - name: IMAGE_NAME - value: ubuntu-18.04 + value: ubuntu-22.04 - name: PYTHON_VERSION value: 3.10 - group: certbot-common @@ -47,13 +47,13 @@ jobs: nginx-compat: TOXENV: nginx_compat linux-integration-rfc2136: - IMAGE_NAME: ubuntu-18.04 + IMAGE_NAME: ubuntu-22.04 PYTHON_VERSION: 3.8 TOXENV: integration-dns-rfc2136 docker-dev: TOXENV: docker_dev le-modification: - IMAGE_NAME: ubuntu-18.04 + IMAGE_NAME: ubuntu-22.04 TOXENV: modification farmtest-apache2: PYTHON_VERSION: 3.8 diff --git a/.azure-pipelines/templates/jobs/packaging-jobs.yml b/.azure-pipelines/templates/jobs/packaging-jobs.yml index 5545e559d..c5dcf16dd 100644 --- a/.azure-pipelines/templates/jobs/packaging-jobs.yml +++ b/.azure-pipelines/templates/jobs/packaging-jobs.yml @@ -1,7 +1,7 @@ jobs: - job: docker_build pool: - vmImage: ubuntu-18.04 + vmImage: ubuntu-22.04 strategy: matrix: amd64: @@ -37,7 +37,7 @@ jobs: - job: docker_run dependsOn: docker_build pool: - vmImage: ubuntu-18.04 + vmImage: ubuntu-22.04 steps: - task: DownloadPipelineArtifact@2 inputs: @@ -116,7 +116,7 @@ jobs: displayName: Run certbot integration tests - job: snaps_build pool: - vmImage: ubuntu-18.04 + vmImage: ubuntu-22.04 strategy: matrix: amd64: @@ -164,7 +164,7 @@ jobs: - job: snap_run dependsOn: snaps_build pool: - vmImage: ubuntu-18.04 + vmImage: ubuntu-22.04 steps: - task: UsePythonVersion@0 inputs: @@ -194,7 +194,7 @@ jobs: - job: snap_dns_run dependsOn: snaps_build pool: - vmImage: ubuntu-18.04 + vmImage: ubuntu-22.04 steps: - script: | set -e diff --git a/.azure-pipelines/templates/jobs/standard-tests-jobs.yml b/.azure-pipelines/templates/jobs/standard-tests-jobs.yml index 4deaf4f2b..cf5e20c0b 100644 --- a/.azure-pipelines/templates/jobs/standard-tests-jobs.yml +++ b/.azure-pipelines/templates/jobs/standard-tests-jobs.yml @@ -5,11 +5,11 @@ jobs: strategy: matrix: macos-py37-cover: - IMAGE_NAME: macOS-10.15 + IMAGE_NAME: macOS-12 PYTHON_VERSION: 3.7 TOXENV: py37-cover macos-py310-cover: - IMAGE_NAME: macOS-10.15 + IMAGE_NAME: macOS-12 PYTHON_VERSION: 3.10 TOXENV: py310-cover windows-py37: @@ -25,42 +25,42 @@ jobs: PYTHON_VERSION: 3.9 TOXENV: integration-certbot linux-oldest-tests-1: - IMAGE_NAME: ubuntu-18.04 + IMAGE_NAME: ubuntu-22.04 PYTHON_VERSION: 3.7 TOXENV: '{acme,apache,apache-v2,certbot}-oldest' linux-oldest-tests-2: - IMAGE_NAME: ubuntu-18.04 + IMAGE_NAME: ubuntu-22.04 PYTHON_VERSION: 3.7 TOXENV: '{dns,nginx}-oldest' linux-py37: - IMAGE_NAME: ubuntu-18.04 + IMAGE_NAME: ubuntu-22.04 PYTHON_VERSION: 3.7 TOXENV: py37 linux-py310-cover: - IMAGE_NAME: ubuntu-18.04 + IMAGE_NAME: ubuntu-22.04 PYTHON_VERSION: 3.10 TOXENV: py310-cover linux-py310-lint: - IMAGE_NAME: ubuntu-18.04 + IMAGE_NAME: ubuntu-22.04 PYTHON_VERSION: 3.10 TOXENV: lint-posix linux-py310-mypy: - IMAGE_NAME: ubuntu-18.04 + IMAGE_NAME: ubuntu-22.04 PYTHON_VERSION: 3.10 TOXENV: mypy-posix linux-integration: - IMAGE_NAME: ubuntu-18.04 + IMAGE_NAME: ubuntu-22.04 PYTHON_VERSION: 3.8 TOXENV: integration ACME_SERVER: pebble apache-compat: - IMAGE_NAME: ubuntu-18.04 + IMAGE_NAME: ubuntu-22.04 TOXENV: apache_compat apacheconftest: - IMAGE_NAME: ubuntu-18.04 + IMAGE_NAME: ubuntu-22.04 TOXENV: apacheconftest-with-pebble nginxroundtrip: - IMAGE_NAME: ubuntu-18.04 + IMAGE_NAME: ubuntu-22.04 TOXENV: nginxroundtrip pool: vmImage: $(IMAGE_NAME) diff --git a/.azure-pipelines/templates/stages/deploy-stage.yml b/.azure-pipelines/templates/stages/deploy-stage.yml index 424ffed10..8144f4bee 100644 --- a/.azure-pipelines/templates/stages/deploy-stage.yml +++ b/.azure-pipelines/templates/stages/deploy-stage.yml @@ -35,7 +35,7 @@ stages: # more info. - job: publish_snap pool: - vmImage: ubuntu-18.04 + vmImage: ubuntu-22.04 variables: - group: certbot-common strategy: @@ -71,7 +71,7 @@ stages: displayName: Publish to Snap store - job: publish_docker pool: - vmImage: ubuntu-18.04 + vmImage: ubuntu-22.04 strategy: matrix: amd64: diff --git a/.azure-pipelines/templates/steps/tox-steps.yml b/.azure-pipelines/templates/steps/tox-steps.yml index 3e5fb995d..fbda960a5 100644 --- a/.azure-pipelines/templates/steps/tox-steps.yml +++ b/.azure-pipelines/templates/steps/tox-steps.yml @@ -12,7 +12,7 @@ steps: set -e sudo apt-get update sudo apt-get install -y --no-install-recommends \ - python-dev \ + python3-dev \ gcc \ libaugeas0 \ libssl-dev \ @@ -36,8 +36,8 @@ steps: # problems with its lack of real dependency resolution. - bash: | set -e - python tools/pipstrap.py - python tools/pip_install.py -I tox virtualenv + python3 tools/pipstrap.py + python3 tools/pip_install.py -I tox virtualenv displayName: Install runtime dependencies - task: DownloadSecureFile@1 name: testFarmPem @@ -49,7 +49,7 @@ steps: export TARGET_BRANCH="`echo "${BUILD_SOURCEBRANCH}" | sed -E 's!refs/(heads|tags)/!!g'`" [ -z "${SYSTEM_PULLREQUEST_TARGETBRANCH}" ] || export TARGET_BRANCH="${SYSTEM_PULLREQUEST_TARGETBRANCH}" env - python -m tox + python3 -m tox env: AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID) AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY) From f7e61edcb2ea3195c9889c407a08e6dffb7f60dc Mon Sep 17 00:00:00 2001 From: alexzorin Date: Wed, 31 Aug 2022 07:41:53 +1000 Subject: [PATCH 15/29] deprecate more attributes in acme (#9369) * deprecate more attributes in acme * Deprecate .Authorization.combinations by renaming the field and deprecating in getters/setters * Silence deprecation warnings from our own imports of acme.mixins Co-authored-by: Brad Warren --- acme/acme/challenges.py | 8 ++++++-- acme/acme/client.py | 9 +++++++-- acme/acme/messages.py | 45 +++++++++++++++++++++++++++++++++++++---- acme/acme/mixins.py | 4 ++++ certbot/CHANGELOG.md | 3 +++ pytest.ini | 6 ++++++ 6 files changed, 67 insertions(+), 8 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 91c3e6f24..534a60aad 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -14,6 +14,7 @@ from typing import Tuple from typing import Type from typing import TypeVar from typing import Union +import warnings from cryptography.hazmat.primitives import hashes import josepy as jose @@ -24,8 +25,11 @@ import requests from acme import crypto_util from acme import errors from acme import fields -from acme.mixins import ResourceMixin -from acme.mixins import TypeMixin + +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + from acme.mixins import ResourceMixin + from acme.mixins import TypeMixin logger = logging.getLogger(__name__) diff --git a/acme/acme/client.py b/acme/acme/client.py index dbb2a23f1..be4c2e457 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -45,7 +45,9 @@ from acme import crypto_util from acme import errors from acme import jws from acme import messages -from acme.mixins import VersionedLEACMEMixin +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + from acme.mixins import VersionedLEACMEMixin logger = logging.getLogger(__name__) @@ -57,6 +59,9 @@ DER_CONTENT_TYPE = 'application/pkix-cert' class ClientBase: """ACME client base object. + .. deprecated:: 1.30.0 + Use `ClientV2` instead. + :ivar messages.Directory directory: :ivar .ClientNetwork net: Client network. :ivar int acme_version: ACME protocol version. 1 or 2. @@ -1312,7 +1317,7 @@ class _ClientDeprecationModule: self.__dict__['_module'] = module def __getattr__(self, attr: str) -> Any: - if attr in ('Client', 'BackwardsCompatibleClientV2'): + if attr in ('Client', 'ClientBase', 'BackwardsCompatibleClientV2'): warnings.warn('The {0} attribute in acme.client is deprecated ' 'and will be removed soon.'.format(attr), DeprecationWarning, stacklevel=2) diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 9b9ef5de2..fbb7738d0 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -14,6 +14,7 @@ from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union +import warnings import josepy as jose @@ -22,7 +23,9 @@ from acme import errors from acme import fields from acme import jws from acme import util -from acme.mixins import ResourceMixin +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + from acme.mixins import ResourceMixin if TYPE_CHECKING: from typing_extensions import Protocol # pragma: no cover @@ -573,14 +576,14 @@ class Authorization(ResourceBody): :ivar acme.messages.Identifier identifier: :ivar list challenges: `list` of `.ChallengeBody` :ivar tuple combinations: Challenge combinations (`tuple` of `tuple` - of `int`, as opposed to `list` of `list` from the spec). + of `int`, as opposed to `list` of `list` from the spec). (deprecated since 1.30.0) :ivar acme.messages.Status status: :ivar datetime.datetime expires: """ identifier: Identifier = jose.field('identifier', decoder=Identifier.from_json, omitempty=True) challenges: List[ChallengeBody] = jose.field('challenges', omitempty=True) - combinations: Tuple[Tuple[int, ...], ...] = jose.field('combinations', omitempty=True) + _combinations: Tuple[Tuple[int, ...], ...] = jose.field('combinations', omitempty=True) status: Status = jose.field('status', omitempty=True, decoder=Status.from_json) # TODO: 'expires' is allowed for Authorization Resources in @@ -590,15 +593,49 @@ class Authorization(ResourceBody): expires: datetime.datetime = fields.rfc3339('expires', omitempty=True) wildcard: bool = jose.field('wildcard', omitempty=True) + # combinations is temporarily renamed to _combinations during its deprecation + # period. See https://github.com/certbot/certbot/pull/9369#issuecomment-1199849262. + def __init__(self, **kwargs: Any) -> None: + if 'combinations' in kwargs: + kwargs['_combinations'] = kwargs.pop('combinations') + super().__init__(**kwargs) + # Mypy does not understand the josepy magic happening here, and falsely claims # that challenge is redefined. Let's ignore the type check here. @challenges.decoder # type: ignore def challenges(value: List[Dict[str, Any]]) -> Tuple[ChallengeBody, ...]: # type: ignore[misc] # pylint: disable=no-self-argument,missing-function-docstring return tuple(ChallengeBody.from_json(chall) for chall in value) + @property + def combinations(self) -> Tuple[Tuple[int, ...], ...]: + """Challenge combinations. + (`tuple` of `tuple` of `int`, as opposed to `list` of `list` from the spec). + + .. deprecated: 1.30.0 + + """ + warnings.warn( + "acme.messages.Authorization.combinations is deprecated and will be " + "removed in a future release.", DeprecationWarning) + return self._combinations + + @combinations.setter + def combinations(self, combos: Tuple[Tuple[int, ...], ...]) -> None: # pragma: no cover + warnings.warn( + "acme.messages.Authorization.combinations is deprecated and will be " + "removed in a future release.", DeprecationWarning) + self._combinations = combos + @property def resolved_combinations(self) -> Tuple[Tuple[ChallengeBody, ...], ...]: - """Combinations with challenges instead of indices.""" + """Combinations with challenges instead of indices. + + .. deprecated: 1.30.0 + + """ + warnings.warn( + "acme.messages.Authorization.resolved_combinations is deprecated and will be " + "removed in a future release.", DeprecationWarning) return tuple(tuple(self.challenges[idx] for idx in combo) for combo in self.combinations) # pylint: disable=not-an-iterable diff --git a/acme/acme/mixins.py b/acme/acme/mixins.py index e6e678d60..4c52957a5 100644 --- a/acme/acme/mixins.py +++ b/acme/acme/mixins.py @@ -1,6 +1,10 @@ """Useful mixins for Challenge and Resource objects""" from typing import Any from typing import Dict +import warnings + +warnings.warn(f'The module {__name__} is deprecated and will be removed in a future release', + DeprecationWarning, stacklevel=2) class VersionedLEACMEMixin: diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 90df55e53..04b25e98d 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -10,6 +10,9 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). ### Changed +* `acme.client.ClientBase`, `acme.messages.Authorization.resolved_combinations`, + `acme.messages.Authorization.combinations` and `acme.mixins` are deprecated and + will be removed in a future release. * The `certbot-dns-cloudxns` plugin is now deprecated and will be removed in the next major release of Certbot. * The `source_address` argument for `acme.client.ClientNetwork` is deprecated diff --git a/pytest.ini b/pytest.ini index 9e8fb5c7b..78710566b 100644 --- a/pytest.ini +++ b/pytest.ini @@ -24,6 +24,9 @@ # certbot-dns-rfc2136. # 6) botocore is currently using deprecated urllib3 functionality. See # https://github.com/boto/botocore/issues/2744. +# 7) ACMEv1 deprecations in acme.client which will be resolved by Certbot 2.0. +# 8) acme.mixins deprecation in acme.client which will be resolved by Certbot 2.0. +# 9) acme.messages.Authorization.combinations which will be resolved by Certbot 2.0. filterwarnings = error ignore:The external mock module:PendingDeprecationWarning @@ -32,3 +35,6 @@ filterwarnings = ignore:.*attribute in certbot.display.util module is deprecated:DeprecationWarning ignore:decodestring\(\) is a deprecated alias:DeprecationWarning:dns ignore:'urllib3.contrib.pyopenssl:DeprecationWarning:botocore + ignore:.*attribute in acme.client is deprecated:DeprecationWarning + ignore:.*acme.mixins is deprecated:DeprecationWarning + ignore:.*Authorization.combinations is deprecated:DeprecationWarning From c20d40ddbafb8db9e38cf5ec41b0ec187cc06686 Mon Sep 17 00:00:00 2001 From: alexzorin Date: Fri, 2 Sep 2022 23:55:04 +1000 Subject: [PATCH 16/29] acme: further deprecations (#9395) * acme: deprecate acme.fields.Resource and .resource * acme: deprecate .messages.OLD_ERROR_PREFIX * acme: deprecate .messages.Directory.register * acme: clean up deprecations * dont use unscoped filterwarnings * change deprecation approach for acme.fields * warn on non-string keys in acme.messages.Directory * remove leaked filterwarnings in BackwardsCompatibleClientV2Test * remove non-string lookups of acme.messages.Directory --- acme/acme/challenges.py | 4 +- acme/acme/client.py | 4 +- acme/acme/fields.py | 51 +++++++++- acme/acme/messages.py | 180 +++++++++++++++++++++++------------- acme/tests/client_test.py | 31 ++++--- acme/tests/fields_test.py | 7 +- acme/tests/messages_test.py | 17 ++-- certbot/CHANGELOG.md | 10 +- 8 files changed, 211 insertions(+), 93 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 534a60aad..194f19e47 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -56,7 +56,9 @@ class ChallengeResponse(ResourceMixin, TypeMixin, jose.TypedJSONObjectWithFields """ACME challenge response.""" TYPES: Dict[str, Type['ChallengeResponse']] = {} resource_type = 'challenge' - resource: str = fields.resource(resource_type) + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', 'resource attribute in acme.fields', DeprecationWarning) + resource: str = fields.resource(resource_type) class UnrecognizedChallenge(Challenge): diff --git a/acme/acme/client.py b/acme/acme/client.py index be4c2e457..7d21b0fad 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -306,7 +306,7 @@ class Client(ClientBase): """ new_reg = messages.NewRegistration() if new_reg is None else new_reg - response = self._post(self.directory[new_reg], new_reg) + response = self._post(self.directory['new-reg'], new_reg) # TODO: handle errors assert response.status_code == http_client.CREATED @@ -612,7 +612,7 @@ class Client(ClientBase): :raises .ClientError: If revocation is unsuccessful. """ - self._revoke(cert, rsn, self.directory[messages.Revocation]) + self._revoke(cert, rsn, self.directory['revoke-cert']) class ClientV2(ClientBase): diff --git a/acme/acme/fields.py b/acme/acme/fields.py index 191231df2..8a1dc8462 100644 --- a/acme/acme/fields.py +++ b/acme/acme/fields.py @@ -1,8 +1,12 @@ """ACME JSON fields.""" import datetime -from typing import Any - import logging +import sys +from types import ModuleType +from typing import Any +from typing import cast +from typing import List +import warnings import josepy as jose import pyrfc3339 @@ -52,7 +56,11 @@ class RFC3339Field(jose.Field): class Resource(jose.Field): - """Resource MITM field.""" + """Resource MITM field. + + .. deprecated: 1.30.0 + + """ def __init__(self, resource_type: str, *args: Any, **kwargs: Any) -> None: self.resource_type = resource_type @@ -78,5 +86,40 @@ def rfc3339(json_name: str, omitempty: bool = False) -> Any: def resource(resource_type: str) -> Any: - """Generates a type-friendly Resource field.""" + """Generates a type-friendly Resource field. + + .. deprecated: 1.30.0 + + """ return Resource(resource_type) + + +# This class takes a similar approach to the cryptography project to deprecate attributes +# in public modules. See the _ModuleWithDeprecation class here: +# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 +class _FieldsDeprecationModule: # pragma: no cover + """ + Internal class delegating to a module, and displaying warnings when + module attributes deprecated in acme.fields are accessed. + """ + def __init__(self, module: ModuleType) -> None: + self.__dict__['_module'] = module + + def __getattr__(self, attr: str) -> None: + if attr in ('Resource', 'resource'): + warnings.warn('{0} attribute in acme.fields module is deprecated ' + 'and will be removed soon.'.format(attr), + DeprecationWarning, stacklevel=2) + return getattr(self._module, attr) + + def __setattr__(self, attr: str, value: Any) -> None: + setattr(self._module, attr, value) + + def __delattr__(self, attr: str) -> None: + delattr(self._module, attr) + + def __dir__(self) -> List[str]: + return ['_module'] + dir(self._module) + + +sys.modules[__name__] = cast(ModuleType, _FieldsDeprecationModule(sys.modules[__name__])) diff --git a/acme/acme/messages.py b/acme/acme/messages.py index fbb7738d0..cdefcfa4f 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -2,7 +2,9 @@ import datetime from collections.abc import Hashable import json +from types import ModuleType from typing import Any +from typing import cast from typing import Dict from typing import Iterator from typing import List @@ -14,6 +16,7 @@ from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union +import sys import warnings import josepy as jose @@ -24,7 +27,7 @@ from acme import fields from acme import jws from acme import util with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + warnings.filterwarnings("ignore", ".*acme.mixins", category=DeprecationWarning) from acme.mixins import ResourceMixin if TYPE_CHECKING: @@ -277,6 +280,10 @@ class Directory(jose.JSONDeSerializable): def register(cls, resource_body_cls: Type[GenericHasResourceType]) -> Type[GenericHasResourceType]: """Register resource.""" + warnings.warn( + "acme.messages.Directory.register is deprecated and will be removed in the next " + "major release of Certbot", DeprecationWarning, stacklevel=2 + ) resource_type = resource_body_cls.resource_type assert resource_type not in cls._REGISTERED_TYPES cls._REGISTERED_TYPES[resource_type] = resource_body_cls @@ -295,6 +302,12 @@ class Directory(jose.JSONDeSerializable): raise AttributeError(str(error)) def __getitem__(self, name: Union[str, HasResourceType, Type[HasResourceType]]) -> Any: + if not isinstance(name, str): + warnings.warn( + "Looking up acme.messages.Directory resources by non-string keys is deprecated " + "and will be removed in the next major release of Certbot", + DeprecationWarning, stacklevel=2 + ) try: return self._jobj[self._canon_key(name)] except KeyError: @@ -462,19 +475,6 @@ class Registration(ResourceBody): return self._filter_contact(self.email_prefix) -@Directory.register -class NewRegistration(ResourceMixin, Registration): - """New registration.""" - resource_type = 'new-reg' - resource: str = fields.resource(resource_type) - - -class UpdateRegistration(ResourceMixin, Registration): - """Update registration.""" - resource_type = 'reg' - resource: str = fields.resource(resource_type) - - class RegistrationResource(ResourceWithURI): """Registration Resource. @@ -616,14 +616,14 @@ class Authorization(ResourceBody): """ warnings.warn( "acme.messages.Authorization.combinations is deprecated and will be " - "removed in a future release.", DeprecationWarning) + "removed in a future release.", DeprecationWarning, stacklevel=2) return self._combinations @combinations.setter def combinations(self, combos: Tuple[Tuple[int, ...], ...]) -> None: # pragma: no cover warnings.warn( "acme.messages.Authorization.combinations is deprecated and will be " - "removed in a future release.", DeprecationWarning) + "removed in a future release.", DeprecationWarning, stacklevel=2) self._combinations = combos @property @@ -635,22 +635,11 @@ class Authorization(ResourceBody): """ warnings.warn( "acme.messages.Authorization.resolved_combinations is deprecated and will be " - "removed in a future release.", DeprecationWarning) - return tuple(tuple(self.challenges[idx] for idx in combo) - for combo in self.combinations) # pylint: disable=not-an-iterable - - -@Directory.register -class NewAuthorization(ResourceMixin, Authorization): - """New authorization.""" - resource_type = 'new-authz' - resource: str = fields.resource(resource_type) - - -class UpdateAuthorization(ResourceMixin, Authorization): - """Update authorization.""" - resource_type = 'authz' - resource: str = fields.resource(resource_type) + "removed in a future release.", DeprecationWarning, stacklevel=2) + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', '.*combinations', DeprecationWarning) + return tuple(tuple(self.challenges[idx] for idx in combo) + for combo in self.combinations) # pylint: disable=not-an-iterable class AuthorizationResource(ResourceWithURI): @@ -664,19 +653,6 @@ class AuthorizationResource(ResourceWithURI): new_cert_uri: str = jose.field('new_cert_uri', omitempty=True) -@Directory.register -class CertificateRequest(ResourceMixin, jose.JSONObjectWithFields): - """ACME new-cert request. - - :ivar jose.ComparableX509 csr: - `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` - - """ - resource_type = 'new-cert' - resource: str = fields.resource(resource_type) - csr: jose.ComparableX509 = jose.field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) - - class CertificateResource(ResourceWithURI): """Certificate Resource. @@ -690,21 +666,6 @@ class CertificateResource(ResourceWithURI): authzrs: Tuple[AuthorizationResource, ...] = jose.field('authzrs') -@Directory.register -class Revocation(ResourceMixin, jose.JSONObjectWithFields): - """Revocation message. - - :ivar jose.ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in - `jose.ComparableX509` - - """ - resource_type = 'revoke-cert' - resource: str = fields.resource(resource_type) - certificate: jose.ComparableX509 = jose.field( - 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) - reason: int = jose.field('reason') - - class Order(ResourceBody): """Order Resource Body. @@ -756,7 +717,98 @@ class OrderResource(ResourceWithURI): omitempty=True) -@Directory.register -class NewOrder(Order): - """New order.""" - resource_type = 'new-order' +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "acme.messages.Directory.register", DeprecationWarning) + warnings.filterwarnings("ignore", "resource attribute in acme.fields", DeprecationWarning) + + @Directory.register + class NewOrder(Order): + """New order.""" + resource_type = 'new-order' + + + @Directory.register + class Revocation(ResourceMixin, jose.JSONObjectWithFields): + """Revocation message. + + :ivar jose.ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in + `jose.ComparableX509` + + """ + resource_type = 'revoke-cert' + resource: str = fields.resource(resource_type) + certificate: jose.ComparableX509 = jose.field( + 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) + reason: int = jose.field('reason') + + + @Directory.register + class CertificateRequest(ResourceMixin, jose.JSONObjectWithFields): + """ACME new-cert request. + + :ivar jose.ComparableX509 csr: + `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` + + """ + resource_type = 'new-cert' + resource: str = fields.resource(resource_type) + csr: jose.ComparableX509 = jose.field('csr', decoder=jose.decode_csr, + encoder=jose.encode_csr) + + + @Directory.register + class NewAuthorization(ResourceMixin, Authorization): + """New authorization.""" + resource_type = 'new-authz' + resource: str = fields.resource(resource_type) + + + class UpdateAuthorization(ResourceMixin, Authorization): + """Update authorization.""" + resource_type = 'authz' + resource: str = fields.resource(resource_type) + + + @Directory.register + class NewRegistration(ResourceMixin, Registration): + """New registration.""" + resource_type = 'new-reg' + resource: str = fields.resource(resource_type) + + + class UpdateRegistration(ResourceMixin, Registration): + """Update registration.""" + resource_type = 'reg' + resource: str = fields.resource(resource_type) + + +# This class takes a similar approach to the cryptography project to deprecate attributes +# in public modules. See the _ModuleWithDeprecation class here: +# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 +class _MessagesDeprecationModule: # pragma: no cover + """ + Internal class delegating to a module, and displaying warnings when + module attributes deprecated in acme.messages are accessed. + """ + def __init__(self, module: ModuleType) -> None: + self.__dict__['_module'] = module + + def __getattr__(self, attr: str) -> None: + if attr == 'OLD_ERROR_PREFIX': + warnings.warn('{0} attribute in acme.messages module is deprecated ' + 'and will be removed soon.'.format(attr), + DeprecationWarning, stacklevel=2) + return getattr(self._module, attr) + + def __setattr__(self, attr: str, value: Any) -> None: + setattr(self._module, attr, value) + + def __delattr__(self, attr: str) -> None: + delattr(self._module, attr) + + def __dir__(self) -> List[str]: + return ['_module'] + dir(self._module) + + +# Patching ourselves to warn about acme.messages.OLD_ERROR_PREFIX deprecation and planned removal. +sys.modules[__name__] = cast(ModuleType, _MessagesDeprecationModule(sys.modules[__name__])) diff --git a/acme/tests/client_test.py b/acme/tests/client_test.py index 1d9aa27fe..e717f734a 100644 --- a/acme/tests/client_test.py +++ b/acme/tests/client_test.py @@ -3,11 +3,11 @@ import copy import datetime import http.client as http_client -import ipaddress import json import unittest from typing import Dict from unittest import mock +import warnings import josepy as jose import OpenSSL @@ -17,9 +17,17 @@ from acme import challenges from acme import errors from acme import jws as acme_jws from acme import messages +from acme.client import ClientNetwork +from acme.client import ClientV2 from acme.mixins import VersionedLEACMEMixin import messages_test import test_util +# Remove the following in Certbot 2.0: +with warnings.catch_warnings(): + warnings.filterwarnings('ignore', '.* in acme.client', DeprecationWarning) + from acme.client import BackwardsCompatibleClientV2 + from acme.client import Client + CERT_DER = test_util.load_vector('cert.der') CERT_SAN_PEM = test_util.load_vector('cert-san.pem') @@ -87,12 +95,17 @@ class ClientTestBase(unittest.TestCase): # Reason code for revocation self.rsn = 1 - class BackwardsCompatibleClientV2Test(ClientTestBase): """Tests for acme.client.BackwardsCompatibleClientV2.""" def setUp(self): super().setUp() + + # For some reason, required to suppress warnings on mock.patch('acme.client.Client') + self.warning_cap = warnings.catch_warnings() + self.warning_cap.__enter__() + warnings.filterwarnings('ignore', '.*acme.client', DeprecationWarning) + # contains a loaded cert self.certr = messages.CertificateResource( body=messages_test.CERT) @@ -114,15 +127,17 @@ class BackwardsCompatibleClientV2Test(ClientTestBase): self.orderr = messages.OrderResource( csr_pem=CSR_SAN_PEM) + def tearDown(self) -> None: + self.warning_cap.__exit__() + return super().tearDown() + def _init(self): uri = 'http://www.letsencrypt-demo.org/directory' - from acme.client import BackwardsCompatibleClientV2 return BackwardsCompatibleClientV2(net=self.net, key=KEY, server=uri) def test_init_downloads_directory(self): uri = 'http://www.letsencrypt-demo.org/directory' - from acme.client import BackwardsCompatibleClientV2 BackwardsCompatibleClientV2(net=self.net, key=KEY, server=uri) self.net.get.assert_called_once_with(uri) @@ -336,13 +351,11 @@ class ClientTest(ClientTestBase): uri='https://www.letsencrypt-demo.org/acme/cert/1', cert_chain_uri='https://www.letsencrypt-demo.org/ca') - from acme.client import Client self.client = Client( directory=self.directory, key=KEY, alg=jose.RS256, net=self.net) def test_init_downloads_directory(self): uri = 'http://www.letsencrypt-demo.org/directory' - from acme.client import Client self.client = Client( directory=uri, key=KEY, alg=jose.RS256, net=self.net) self.net.get.assert_called_once_with(uri) @@ -351,7 +364,6 @@ class ClientTest(ClientTestBase): def test_init_without_net(self, mock_net): mock_net.return_value = mock.sentinel.net alg = jose.RS256 - from acme.client import Client self.client = Client( directory=self.directory, key=KEY, alg=alg) mock_net.called_once_with(KEY, alg=alg, verify_ssl=True) @@ -723,7 +735,6 @@ class ClientV2Test(ClientTestBase): self.directory = DIRECTORY_V2 - from acme.client import ClientV2 self.client = ClientV2(self.directory, self.net) self.new_reg = self.new_reg.update(terms_of_service_agreed=True) @@ -948,7 +959,6 @@ class ClientNetworkTest(unittest.TestCase): self.verify_ssl = mock.MagicMock() self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped) - from acme.client import ClientNetwork self.net = ClientNetwork( key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl, user_agent='acme-python-test') @@ -1179,7 +1189,6 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): """Tests for acme.client.ClientNetwork which mock out response.""" def setUp(self): - from acme.client import ClientNetwork self.net = ClientNetwork(key=None, alg=None) self.response = mock.MagicMock(ok=True, status_code=http_client.OK) @@ -1342,7 +1351,6 @@ class ClientNetworkSourceAddressBindingTest(unittest.TestCase): self.source_address = "8.8.8.8" def test_source_address_set(self): - from acme.client import ClientNetwork with mock.patch('warnings.warn') as mock_warn: net = ClientNetwork(key=None, alg=None, source_address=self.source_address) mock_warn.assert_called_once() @@ -1353,7 +1361,6 @@ class ClientNetworkSourceAddressBindingTest(unittest.TestCase): def test_behavior_assumption(self): """This is a test that guardrails the HTTPAdapter behavior so that if the default for a Session() changes, the assumptions here aren't violated silently.""" - from acme.client import ClientNetwork # Source address not specified, so the default adapter type should be bound -- this # test should fail if the default adapter type is changed by requests net = ClientNetwork(key=None, alg=None) diff --git a/acme/tests/fields_test.py b/acme/tests/fields_test.py index 4cc167f9c..76b215342 100644 --- a/acme/tests/fields_test.py +++ b/acme/tests/fields_test.py @@ -1,6 +1,7 @@ """Tests for acme.fields.""" import datetime import unittest +import warnings import josepy as jose import pytz @@ -58,8 +59,10 @@ class ResourceTest(unittest.TestCase): """Tests for acme.fields.Resource.""" def setUp(self): - from acme.fields import Resource - self.field = Resource('x') + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', '.*Resource', DeprecationWarning) + from acme.fields import Resource + self.field = Resource('x') def test_decode_good(self): self.assertEqual('x', self.field.decode('x')) diff --git a/acme/tests/messages_test.py b/acme/tests/messages_test.py index cf7e7629a..782955fb4 100644 --- a/acme/tests/messages_test.py +++ b/acme/tests/messages_test.py @@ -2,6 +2,7 @@ from typing import Dict import unittest from unittest import mock +import warnings import josepy as jose @@ -150,8 +151,10 @@ class DirectoryTest(unittest.TestCase): def test_getitem(self): self.assertEqual('reg', self.dir['new-reg']) from acme.messages import NewRegistration - self.assertEqual('reg', self.dir[NewRegistration]) - self.assertEqual('reg', self.dir[NewRegistration()]) + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', '.* non-string keys', DeprecationWarning) + self.assertEqual('reg', self.dir[NewRegistration]) + self.assertEqual('reg', self.dir[NewRegistration()]) def test_getitem_fails_with_key_error(self): self.assertRaises(KeyError, self.dir.__getitem__, 'foo') @@ -407,10 +410,12 @@ class AuthorizationTest(unittest.TestCase): hash(Authorization.from_json(self.jobj_from)) def test_resolved_combinations(self): - self.assertEqual(self.authz.resolved_combinations, ( - (self.challbs[0],), - (self.challbs[1],), - )) + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', '.*resolved_combinations', DeprecationWarning) + self.assertEqual(self.authz.resolved_combinations, ( + (self.challbs[0],), + (self.challbs[1],), + )) class AuthorizationResourceTest(unittest.TestCase): diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 04b25e98d..22c2ffdba 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -11,8 +11,14 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). ### Changed * `acme.client.ClientBase`, `acme.messages.Authorization.resolved_combinations`, - `acme.messages.Authorization.combinations` and `acme.mixins` are deprecated and - will be removed in a future release. + `acme.messages.Authorization.combinations`, `acme.mixins`, `acme.fields.resource`, + and `acme.fields.Resource` are deprecated and will be removed in a future release. +* `acme.messages.OLD_ERROR_PREFIX` (`urn:acme:error:`) is deprecated and support for + the old ACME error prefix in Certbot will be removed in the next major release of + Certbot. +* `acme.messages.Directory.register` is deprecated and will be removed in the next + major release of Certbot. Furthermore, `.Directory` will only support lookups + by the exact resource name string in the ACME directory (e.g. `directory['newOrder']`). * The `certbot-dns-cloudxns` plugin is now deprecated and will be removed in the next major release of Certbot. * The `source_address` argument for `acme.client.ClientNetwork` is deprecated From 804ca323144c0f354d90460b725417ac8707bcbe Mon Sep 17 00:00:00 2001 From: alexzorin Date: Wed, 7 Sep 2022 07:36:55 +1000 Subject: [PATCH 17/29] acme: remove Client and BackwardsCompatibleClientV2 (#9356) * acme: remove Client and BackwardsCompatibleClientV2 * remove ClientTestBase and some unused variables * add ClientV2.get_directory * tweak ToS callback code * acme: update example to use ClientV2.get_directory * simplify ToS callback further into one step * further removal of acmev1-related code - remove acme.client.ClientBase - remove acme.mixins.VersionedLEACMEMixin - remove acme.client.DER_CONTENT_TYPE - remove various ACMEv1 special cases - remove acme.messages.ChallengeResources.combinations * remove .mixins.ResourceMixin, fields.resource, fields.Resource and resource field from various .message classes. * simplify acme.messages.Directory: - remove Directory.register - remove HasResourceType and GenericHasResourceType - remove ability to look up Directory resources by anything other than the exact field name in RFC8555 (section 9.7.5) * remove acme.messages.OLD_ERROR_PREFIX and support the old prefix * remove acme.mixins * reorder imports * add comment to Directory about resource lookups * s/new-cert/newOrder/ * get rid of `resource` sillyness in tests * remove acmev1 terms-of-service support from directory --- acme/acme/challenges.py | 14 +- acme/acme/client.py | 922 ++++------------------ acme/acme/fields.py | 21 - acme/acme/magic_typing.py | 18 - acme/acme/messages.py | 110 +-- acme/acme/mixins.py | 68 -- acme/examples/http01_example.py | 5 +- acme/tests/challenges_test.py | 12 +- acme/tests/client_test.py | 804 +++---------------- acme/tests/fields_test.py | 14 - acme/tests/magic_typing_test.py | 30 - acme/tests/messages_test.py | 33 +- certbot/certbot/_internal/account.py | 14 +- certbot/certbot/_internal/auth_handler.py | 57 +- certbot/certbot/_internal/client.py | 34 +- certbot/certbot/interfaces.py | 4 +- certbot/certbot/tests/acme_util.py | 13 +- certbot/tests/auth_handler_test.py | 107 +-- certbot/tests/client_test.py | 39 +- certbot/tests/main_test.py | 50 +- 20 files changed, 452 insertions(+), 1917 deletions(-) delete mode 100644 acme/acme/magic_typing.py delete mode 100644 acme/acme/mixins.py delete mode 100644 acme/tests/magic_typing_test.py diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 9000b370a..97e3bed88 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -23,9 +23,6 @@ import requests from acme import crypto_util from acme import errors -from acme import fields -from acme.mixins import ResourceMixin -from acme.mixins import TypeMixin logger = logging.getLogger(__name__) @@ -47,12 +44,17 @@ class Challenge(jose.TypedJSONObjectWithFields): return UnrecognizedChallenge.from_json(jobj) -class ChallengeResponse(ResourceMixin, TypeMixin, jose.TypedJSONObjectWithFields): +class ChallengeResponse(jose.TypedJSONObjectWithFields): # _fields_to_partial_json """ACME challenge response.""" TYPES: Dict[str, Type['ChallengeResponse']] = {} - resource_type = 'challenge' - resource: str = fields.resource(resource_type) + + def to_partial_json(self) -> Dict[str, Any]: + # Removes the `type` field which is inserted by TypedJSONObjectWithFields.to_partial_json. + # This field breaks RFC8555 compliance. + jobj = super().to_partial_json() + jobj.pop(self.type_field_name, None) + return jobj class UnrecognizedChallenge(Challenge): diff --git a/acme/acme/client.py b/acme/acme/client.py index e1dc9040f..105e9298f 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -1,23 +1,13 @@ """ACME client API.""" -# pylint: disable=too-many-lines -# This pylint disable can be deleted once the deprecated ACMEv1 code is -# removed. import base64 -import collections import datetime from email.utils import parsedate_tz -import heapq import http.client as http_client import logging import re -import sys import time -from types import ModuleType from typing import Any -from typing import Callable from typing import cast -from typing import Dict -from typing import Iterable from typing import List from typing import Mapping from typing import Optional @@ -25,7 +15,6 @@ from typing import Set from typing import Text from typing import Tuple from typing import Union -import warnings import josepy as jose import OpenSSL @@ -39,572 +28,13 @@ from acme import crypto_util from acme import errors from acme import jws from acme import messages -from acme.mixins import VersionedLEACMEMixin logger = logging.getLogger(__name__) DEFAULT_NETWORK_TIMEOUT = 45 -DER_CONTENT_TYPE = 'application/pkix-cert' - -class ClientBase: - """ACME client base object. - - :ivar messages.Directory directory: - :ivar .ClientNetwork net: Client network. - :ivar int acme_version: ACME protocol version. 1 or 2. - """ - def __init__(self, directory: messages.Directory, net: 'ClientNetwork', - acme_version: int) -> None: - """Initialize. - - :param .messages.Directory directory: Directory Resource - :param .ClientNetwork net: Client network. - :param int acme_version: ACME protocol version. 1 or 2. - """ - self.directory = directory - self.net = net - self.acme_version = acme_version - - @classmethod - def _regr_from_response(cls, response: requests.Response, uri: Optional[str] = None, - terms_of_service: Optional[str] = None - ) -> messages.RegistrationResource: - if 'terms-of-service' in response.links: - terms_of_service = response.links['terms-of-service']['url'] - - return messages.RegistrationResource( - body=messages.Registration.from_json(response.json()), - uri=response.headers.get('Location', uri), - terms_of_service=terms_of_service) - - def _send_recv_regr(self, regr: messages.RegistrationResource, - body: messages.Registration) -> messages.RegistrationResource: - response = self._post(regr.uri, body) - - # TODO: Boulder returns httplib.ACCEPTED - #assert response.status_code == httplib.OK - - # TODO: Boulder does not set Location or Link on update - # (c.f. acme-spec #94) - - return self._regr_from_response( - response, uri=regr.uri, - terms_of_service=regr.terms_of_service) - - def _post(self, *args: Any, **kwargs: Any) -> requests.Response: - """Wrapper around self.net.post that adds the acme_version. - - """ - kwargs.setdefault('acme_version', self.acme_version) - if hasattr(self.directory, 'newNonce'): - kwargs.setdefault('new_nonce_url', getattr(self.directory, 'newNonce')) - return self.net.post(*args, **kwargs) - - def update_registration(self, regr: messages.RegistrationResource, - update: Optional[messages.Registration] = None - ) -> messages.RegistrationResource: - """Update registration. - - :param messages.RegistrationResource regr: Registration Resource. - :param messages.Registration update: Updated body of the - resource. If not provided, body will be taken from `regr`. - - :returns: Updated Registration Resource. - :rtype: `.RegistrationResource` - - """ - update = regr.body if update is None else update - body = messages.UpdateRegistration(**dict(update)) - updated_regr = self._send_recv_regr(regr, body=body) - self.net.account = updated_regr - return updated_regr - - def deactivate_registration(self, regr: messages.RegistrationResource - ) -> messages.RegistrationResource: - """Deactivate registration. - - :param messages.RegistrationResource regr: The Registration Resource - to be deactivated. - - :returns: The Registration resource that was deactivated. - :rtype: `.RegistrationResource` - - """ - return self.update_registration(regr, messages.Registration.from_json( - {"status": "deactivated", "contact": None})) - - def deactivate_authorization(self, - authzr: messages.AuthorizationResource - ) -> messages.AuthorizationResource: - """Deactivate authorization. - - :param messages.AuthorizationResource authzr: The Authorization resource - to be deactivated. - - :returns: The Authorization resource that was deactivated. - :rtype: `.AuthorizationResource` - - """ - body = messages.UpdateAuthorization(status='deactivated') - response = self._post(authzr.uri, body) - return self._authzr_from_response(response, - authzr.body.identifier, authzr.uri) - - def _authzr_from_response(self, response: requests.Response, - identifier: Optional[messages.Identifier] = None, - uri: Optional[str] = None) -> messages.AuthorizationResource: - authzr = messages.AuthorizationResource( - body=messages.Authorization.from_json(response.json()), - uri=response.headers.get('Location', uri)) - if identifier is not None and authzr.body.identifier != identifier: # pylint: disable=no-member - raise errors.UnexpectedUpdate(authzr) - return authzr - - def answer_challenge(self, challb: messages.ChallengeBody, - response: challenges.ChallengeResponse) -> messages.ChallengeResource: - """Answer challenge. - - :param challb: Challenge Resource body. - :type challb: `.ChallengeBody` - - :param response: Corresponding Challenge response - :type response: `.challenges.ChallengeResponse` - - :returns: Challenge Resource with updated body. - :rtype: `.ChallengeResource` - - :raises .UnexpectedUpdate: - - """ - resp = self._post(challb.uri, response) - try: - authzr_uri = resp.links['up']['url'] - except KeyError: - raise errors.ClientError('"up" Link header missing') - challr = messages.ChallengeResource( - authzr_uri=authzr_uri, - body=messages.ChallengeBody.from_json(resp.json())) - # TODO: check that challr.uri == resp.headers['Location']? - if challr.uri != challb.uri: - raise errors.UnexpectedUpdate(challr.uri) - return challr - - @classmethod - def retry_after(cls, response: requests.Response, default: int) -> datetime.datetime: - """Compute next `poll` time based on response ``Retry-After`` header. - - Handles integers and various datestring formats per - https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.37 - - :param requests.Response response: Response from `poll`. - :param int default: Default value (in seconds), used when - ``Retry-After`` header is not present or invalid. - - :returns: Time point when next `poll` should be performed. - :rtype: `datetime.datetime` - - """ - retry_after = response.headers.get('Retry-After', str(default)) - try: - seconds = int(retry_after) - except ValueError: - # The RFC 2822 parser handles all of RFC 2616's cases in modern - # environments (primarily HTTP 1.1+ but also py27+) - when = parsedate_tz(retry_after) - if when is not None: - try: - tz_secs = datetime.timedelta(when[-1] if when[-1] is not None else 0) - return datetime.datetime(*when[:7]) - tz_secs - except (ValueError, OverflowError): - pass - seconds = default - - return datetime.datetime.now() + datetime.timedelta(seconds=seconds) - - def _revoke(self, cert: jose.ComparableX509, rsn: int, url: str) -> None: - """Revoke certificate. - - :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in - `.ComparableX509` - - :param int rsn: Reason code for certificate revocation. - - :param str url: ACME URL to post to - - :raises .ClientError: If revocation is unsuccessful. - - """ - response = self._post(url, - messages.Revocation( - certificate=cert, - reason=rsn)) - if response.status_code != http_client.OK: - raise errors.ClientError( - 'Successful revocation must return HTTP OK status') - - -class Client(ClientBase): - """ACME client for a v1 API. - - .. deprecated:: 1.18.0 - Use :class:`ClientV2` instead. - - .. todo:: - Clean up raised error types hierarchy, document, and handle (wrap) - instances of `.DeserializationError` raised in `from_json()`. - - :ivar messages.Directory directory: - :ivar key: `josepy.JWK` (private) - :ivar alg: `josepy.JWASignature` - :ivar bool verify_ssl: Verify SSL certificates? - :ivar .ClientNetwork net: Client network. Useful for testing. If not - supplied, it will be initialized using `key`, `alg` and - `verify_ssl`. - - """ - - def __init__(self, directory: messages.Directory, key: jose.JWK, - alg: jose.JWASignature=jose.RS256, verify_ssl: bool = True, - net: Optional['ClientNetwork'] = None) -> None: - """Initialize. - - :param directory: Directory Resource (`.messages.Directory`) or - URI from which the resource will be downloaded. - - """ - self.key = key - if net is None: - net = ClientNetwork(key, alg=alg, verify_ssl=verify_ssl) - - if isinstance(directory, str): - directory = messages.Directory.from_json( - net.get(directory).json()) - super().__init__(directory=directory, - net=net, acme_version=1) - - def register(self, new_reg: Optional[messages.NewRegistration] = None - ) -> messages.RegistrationResource: - """Register. - - :param .NewRegistration new_reg: - - :returns: Registration Resource. - :rtype: `.RegistrationResource` - - """ - new_reg = messages.NewRegistration() if new_reg is None else new_reg - response = self._post(self.directory[new_reg], new_reg) - # TODO: handle errors - assert response.status_code == http_client.CREATED - - # "Instance of 'Field' has no key/contact member" bug: - return self._regr_from_response(response) - - def query_registration(self, regr: messages.RegistrationResource - ) -> messages.RegistrationResource: - """Query server about registration. - - :param messages.RegistrationResource regr: Existing Registration - Resource. - - """ - return self._send_recv_regr(regr, messages.UpdateRegistration()) - - def agree_to_tos(self, regr: messages.RegistrationResource - ) -> messages.RegistrationResource: - """Agree to the terms-of-service. - - Agree to the terms-of-service in a Registration Resource. - - :param regr: Registration Resource. - :type regr: `.RegistrationResource` - - :returns: Updated Registration Resource. - :rtype: `.RegistrationResource` - - """ - return self.update_registration( - regr.update(body=regr.body.update(agreement=regr.terms_of_service))) - - def request_challenges(self, identifier: messages.Identifier, - new_authzr_uri: Optional[str] = None) -> messages.AuthorizationResource: - """Request challenges. - - :param .messages.Identifier identifier: Identifier to be challenged. - :param str new_authzr_uri: Deprecated. Do not use. - - :returns: Authorization Resource. - :rtype: `.AuthorizationResource` - - :raises errors.WildcardUnsupportedError: if a wildcard is requested - - """ - if new_authzr_uri is not None: - logger.debug("request_challenges with new_authzr_uri deprecated.") - - if identifier.value.startswith("*"): - raise errors.WildcardUnsupportedError( - "Requesting an authorization for a wildcard name is" - " forbidden by this version of the ACME protocol.") - - new_authz = messages.NewAuthorization(identifier=identifier) - response = self._post(self.directory.new_authz, new_authz) - # TODO: handle errors - assert response.status_code == http_client.CREATED - return self._authzr_from_response(response, identifier) - - def request_domain_challenges(self, domain: str,new_authzr_uri: Optional[str] = None - ) -> messages.AuthorizationResource: - """Request challenges for domain names. - - This is simply a convenience function that wraps around - `request_challenges`, but works with domain names instead of - generic identifiers. See ``request_challenges`` for more - documentation. - - :param str domain: Domain name to be challenged. - :param str new_authzr_uri: Deprecated. Do not use. - - :returns: Authorization Resource. - :rtype: `.AuthorizationResource` - - :raises errors.WildcardUnsupportedError: if a wildcard is requested - - """ - return self.request_challenges(messages.Identifier( - typ=messages.IDENTIFIER_FQDN, value=domain), new_authzr_uri) - - def request_issuance(self, csr: jose.ComparableX509, - authzrs: Iterable[messages.AuthorizationResource] - ) -> messages.CertificateResource: - """Request issuance. - - :param csr: CSR - :type csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` - - :param authzrs: `list` of `.AuthorizationResource` - - :returns: Issued certificate - :rtype: `.messages.CertificateResource` - - """ - assert authzrs, "Authorizations list is empty" - logger.debug("Requesting issuance...") - - # TODO: assert len(authzrs) == number of SANs - req = messages.CertificateRequest(csr=csr) - - content_type = DER_CONTENT_TYPE # TODO: add 'cert_type 'argument - response = self._post( - self.directory.new_cert, - req, - content_type=content_type, - headers={'Accept': content_type}) - - cert_chain_uri = response.links.get('up', {}).get('url') - - try: - uri = response.headers['Location'] - except KeyError: - raise errors.ClientError('"Location" Header missing') - - return messages.CertificateResource( - uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri, - body=jose.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_ASN1, response.content))) - - def poll(self, authzr: messages.AuthorizationResource - ) -> Tuple[messages.AuthorizationResource, requests.Response]: - """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: jose.ComparableX509, - authzrs: Iterable[messages.AuthorizationResource], - mintime: int = 5, max_attempts: int = 10 - ) -> Tuple[messages.CertificateResource, - Tuple[messages.AuthorizationResource, ...]]: - """Poll and request issuance. - - This function polls all provided Authorization Resource URIs - until all challenges are valid, respecting ``Retry-After`` HTTP - headers, and then calls `request_issuance`. - - :param .ComparableX509 csr: CSR (`OpenSSL.crypto.X509Req` - wrapped in `.ComparableX509`) - :param authzrs: `list` of `.AuthorizationResource` - :param int mintime: Minimum time before next attempt, used if - ``Retry-After`` is not present in the response. - :param int max_attempts: Maximum number of attempts (per - authorization) before `PollError` with non-empty ``waiting`` - is raised. - - :returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is - the issued certificate (`.messages.CertificateResource`), - and ``updated_authzrs`` is a `tuple` consisting of updated - Authorization Resources (`.AuthorizationResource`) as - present in the responses from server, and in the same order - as the input ``authzrs``. - :rtype: `tuple` - - :raises PollError: in case of timeout or if some authorization - was marked by the CA as invalid - - """ - assert max_attempts > 0 - attempts: Dict[messages.AuthorizationResource, int] = collections.defaultdict(int) - exhausted = set() - - # priority queue with datetime.datetime (based on Retry-After) as key, - # and original Authorization Resource as value - waiting = [ - (datetime.datetime.now(), index, authzr) - for index, authzr in enumerate(authzrs) - ] - heapq.heapify(waiting) - # mapping between original Authorization Resource and the most - # recently updated one - updated = {authzr: authzr for authzr in authzrs} - - while waiting: - # find the smallest Retry-After, and sleep if necessary - when, index, authzr = heapq.heappop(waiting) - now = datetime.datetime.now() - if when > now: - seconds = (when - now).seconds - logger.debug('Sleeping for %d seconds', seconds) - time.sleep(seconds) - - # Note that we poll with the latest updated Authorization - # URI, which might have a different URI than initial one - updated_authzr, response = self.poll(updated[authzr]) - updated[authzr] = updated_authzr - - attempts[authzr] += 1 - if updated_authzr.body.status not in ( # pylint: disable=no-member - messages.STATUS_VALID, messages.STATUS_INVALID): - if attempts[authzr] < max_attempts: - # push back to the priority queue, with updated retry_after - heapq.heappush(waiting, (self.retry_after( - response, default=mintime), index, authzr)) - else: - exhausted.add(authzr) - - if exhausted or any(authzr.body.status == messages.STATUS_INVALID - for authzr in updated.values()): - raise errors.PollError(exhausted, updated) - - updated_authzrs = tuple(updated[authzr] for authzr in authzrs) - return self.request_issuance(csr, updated_authzrs), updated_authzrs - - def _get_cert(self, uri: str) -> Tuple[requests.Response, jose.ComparableX509]: - """Returns certificate from URI. - - :param str uri: URI of certificate - - :returns: tuple of the form - (response, :class:`josepy.util.ComparableX509`) - :rtype: tuple - - """ - content_type = DER_CONTENT_TYPE # TODO: make it a param - response = self.net.get(uri, headers={'Accept': content_type}, - content_type=content_type) - return response, jose.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_ASN1, response.content)) - - def check_cert(self, certr: messages.CertificateResource) -> messages.CertificateResource: - """Check for new cert. - - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :returns: Updated Certificate Resource. - :rtype: `.CertificateResource` - - """ - # TODO: acme-spec 5.1 table action should be renamed to - # "refresh cert", and this method integrated with self.refresh - response, cert = self._get_cert(certr.uri) - if 'Location' not in response.headers: - raise errors.ClientError('Location header missing') - if response.headers['Location'] != certr.uri: - raise errors.UnexpectedUpdate(response.text) - return certr.update(body=cert) - - def refresh(self, certr: messages.CertificateResource) -> messages.CertificateResource: - """Refresh certificate. - - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :returns: Updated Certificate Resource. - :rtype: `.CertificateResource` - - """ - # TODO: If a client sends a refresh request and the server is - # not willing to refresh the certificate, the server MUST - # respond with status code 403 (Forbidden) - return self.check_cert(certr) - - def fetch_chain(self, certr: messages.CertificateResource, - max_length: int = 10) -> List[jose.ComparableX509]: - """Fetch chain for certificate. - - :param .CertificateResource certr: Certificate Resource - :param int max_length: Maximum allowed length of the chain. - Note that each element in the certificate requires new - ``HTTP GET`` request, and the length of the chain is - controlled by the ACME CA. - - :raises errors.Error: if recursion exceeds `max_length` - - :returns: Certificate chain for the Certificate Resource. It is - a list ordered so that the first element is a signer of the - certificate from Certificate Resource. Will be empty if - ``cert_chain_uri`` is ``None``. - :rtype: `list` of `OpenSSL.crypto.X509` wrapped in `.ComparableX509` - - """ - chain: List[jose.ComparableX509] = [] - uri = certr.cert_chain_uri - while uri is not None and len(chain) < max_length: - response, cert = self._get_cert(uri) - uri = response.links.get('up', {}).get('url') - chain.append(cert) - if uri is not None: - raise errors.Error( - "Recursion limit reached. Didn't get {0}".format(uri)) - return chain - - def revoke(self, cert: jose.ComparableX509, rsn: int) -> None: - """Revoke certificate. - - :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in - `.ComparableX509` - - :param int rsn: Reason code for certificate revocation. - - :raises .ClientError: If revocation is unsuccessful. - - """ - self._revoke(cert, rsn, self.directory[messages.Revocation]) - - -class ClientV2(ClientBase): +class ClientV2: """ACME client for a v2 API. :ivar messages.Directory directory: @@ -617,7 +47,8 @@ class ClientV2(ClientBase): :param .messages.Directory directory: Directory Resource :param .ClientNetwork net: Client network. """ - super().__init__(directory=directory, net=net, acme_version=2) + self.directory = directory + self.net = net def new_account(self, new_account: messages.NewRegistration) -> messages.RegistrationResource: """Register. @@ -664,8 +95,13 @@ class ClientV2(ClientBase): """ # https://github.com/certbot/certbot/issues/6155 - new_regr = self._get_v2_account(regr) - return super().update_registration(new_regr, update) + regr = self._get_v2_account(regr) + + update = regr.body if update is None else update + body = messages.UpdateRegistration(**dict(update)) + updated_regr = self._send_recv_regr(regr, body=body) + self.net.account = updated_regr + return updated_regr def _get_v2_account(self, regr: messages.RegistrationResource, update_body: bool = False ) -> messages.RegistrationResource: @@ -827,7 +263,9 @@ class ClientV2(ClientBase): def external_account_required(self) -> bool: """Checks if ACME server requires External Account Binding authentication.""" - return hasattr(self.directory, 'meta') and self.directory.meta.external_account_required + return hasattr(self.directory, 'meta') and \ + hasattr(self.directory.meta, 'external_account_required') and \ + self.directory.meta.external_account_required def _post_as_get(self, *args: Any, **kwargs: Any) -> requests.Response: """ @@ -853,138 +291,156 @@ class ClientV2(ClientBase): return [l['url'] for l in links if 'rel' in l and 'url' in l and l['rel'] == relation_type] + @classmethod + def get_directory(cls, url: str, net: 'ClientNetwork') -> messages.Directory: + """ + Retrieves the ACME directory (RFC 8555 section 7.1.1) from the ACME server. + :param str url: the URL where the ACME directory is available + :param ClientNetwork net: the ClientNetwork to use to make the request -class BackwardsCompatibleClientV2: - """ACME client wrapper that tends towards V2-style calls, but - supports V1 servers. + :returns: the ACME directory object + :rtype: messages.Directory + """ + return messages.Directory.from_json(net.get(url).json()) - .. deprecated:: 1.18.0 - Use :class:`ClientV2` instead. - - .. note:: While this class handles the majority of the differences - between versions of the ACME protocol, if you need to support an - ACME server based on version 3 or older of the IETF ACME draft - that uses combinations in authorizations (or lack thereof) to - signal that the client needs to complete something other than - any single challenge in the authorization to make it valid, the - user of this class needs to understand and handle these - differences themselves. This does not apply to either of Let's - Encrypt's endpoints where successfully completing any challenge - in an authorization will make it valid. - - :ivar int acme_version: 1 or 2, corresponding to the Let's Encrypt endpoint - :ivar .ClientBase client: either Client or ClientV2 - """ - - def __init__(self, net: 'ClientNetwork', key: jose.JWK, server: str) -> None: - directory = messages.Directory.from_json(net.get(server).json()) - self.acme_version = self._acme_version_from_directory(directory) - self.client: Union[Client, ClientV2] - if self.acme_version == 1: - self.client = Client(directory, key=key, net=net) - else: - self.client = ClientV2(directory, net=net) - - def __getattr__(self, name: str) -> Any: - return getattr(self.client, name) - - def new_account_and_tos(self, regr: messages.NewRegistration, - check_tos_cb: Optional[Callable[[str], None]] = None + @classmethod + def _regr_from_response(cls, response: requests.Response, uri: Optional[str] = None, + terms_of_service: Optional[str] = None ) -> messages.RegistrationResource: - """Combined register and agree_tos for V1, new_account for V2 + if 'terms-of-service' in response.links: + terms_of_service = response.links['terms-of-service']['url'] - :param .NewRegistration regr: - :param callable check_tos_cb: callback that raises an error if - the check does not work - """ - def _assess_tos(tos: str) -> None: - if check_tos_cb is not None: - check_tos_cb(tos) - if self.acme_version == 1: - client_v1 = cast(Client, self.client) - regr_res = client_v1.register(regr) - if regr_res.terms_of_service is not None: - _assess_tos(regr_res.terms_of_service) - return client_v1.agree_to_tos(regr_res) - return regr_res - else: - client_v2 = cast(ClientV2, self.client) - if ("terms_of_service" in client_v2.directory.meta and - client_v2.directory.meta.terms_of_service is not None): - _assess_tos(client_v2.directory.meta.terms_of_service) - regr = regr.update(terms_of_service_agreed=True) - return client_v2.new_account(regr) + return messages.RegistrationResource( + body=messages.Registration.from_json(response.json()), + uri=response.headers.get('Location', uri), + terms_of_service=terms_of_service) - def new_order(self, csr_pem: bytes) -> messages.OrderResource: - """Request a new Order object from the server. + def _send_recv_regr(self, regr: messages.RegistrationResource, + body: messages.Registration) -> messages.RegistrationResource: + response = self._post(regr.uri, body) - If using ACMEv1, returns a dummy OrderResource with only - the authorizations field filled in. + # TODO: Boulder returns httplib.ACCEPTED + #assert response.status_code == httplib.OK - :param bytes csr_pem: A CSR in PEM format. + # TODO: Boulder does not set Location or Link on update + # (c.f. acme-spec #94) - :returns: The newly created order. - :rtype: OrderResource + return self._regr_from_response( + response, uri=regr.uri, + terms_of_service=regr.terms_of_service) - :raises errors.WildcardUnsupportedError: if a wildcard domain is - requested but unsupported by the ACME version + def _post(self, *args: Any, **kwargs: Any) -> requests.Response: + """Wrapper around self.net.post that adds the newNonce URL. + + This is used to retry the request in case of a badNonce error. """ - if self.acme_version == 1: - client_v1 = cast(Client, self.client) - csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem) - # pylint: disable=protected-access - dnsNames = crypto_util._pyopenssl_cert_or_req_all_names(csr) - authorizations = [] - for domain in dnsNames: - authorizations.append(client_v1.request_domain_challenges(domain)) - return messages.OrderResource(authorizations=authorizations, csr_pem=csr_pem) - return cast(ClientV2, self.client).new_order(csr_pem) + kwargs.setdefault('new_nonce_url', getattr(self.directory, 'newNonce')) + return self.net.post(*args, **kwargs) - def finalize_order(self, orderr: messages.OrderResource, deadline: datetime.datetime, - fetch_alternative_chains: bool = False) -> messages.OrderResource: - """Finalize an order and obtain a certificate. + def deactivate_registration(self, regr: messages.RegistrationResource + ) -> messages.RegistrationResource: + """Deactivate registration. - :param messages.OrderResource orderr: order to finalize - :param datetime.datetime deadline: when to stop polling and timeout - :param bool fetch_alternative_chains: whether to also fetch alternative - certificate chains + :param messages.RegistrationResource regr: The Registration Resource + to be deactivated. - :returns: finalized order - :rtype: messages.OrderResource + :returns: The Registration resource that was deactivated. + :rtype: `.RegistrationResource` """ - if self.acme_version == 1: - client_v1 = cast(Client, self.client) - csr_pem = orderr.csr_pem - certr = client_v1.request_issuance( - jose.ComparableX509( - OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)), - orderr.authorizations) + return self.update_registration(regr, messages.Registration.from_json( + {"status": "deactivated", "contact": None})) - chain = None - while datetime.datetime.now() < deadline: + def deactivate_authorization(self, + authzr: messages.AuthorizationResource + ) -> messages.AuthorizationResource: + """Deactivate authorization. + + :param messages.AuthorizationResource authzr: The Authorization resource + to be deactivated. + + :returns: The Authorization resource that was deactivated. + :rtype: `.AuthorizationResource` + + """ + body = messages.UpdateAuthorization(status='deactivated') + response = self._post(authzr.uri, body) + return self._authzr_from_response(response, + authzr.body.identifier, authzr.uri) + + def _authzr_from_response(self, response: requests.Response, + identifier: Optional[messages.Identifier] = None, + uri: Optional[str] = None) -> messages.AuthorizationResource: + authzr = messages.AuthorizationResource( + body=messages.Authorization.from_json(response.json()), + uri=response.headers.get('Location', uri)) + if identifier is not None and authzr.body.identifier != identifier: # pylint: disable=no-member + raise errors.UnexpectedUpdate(authzr) + return authzr + + def answer_challenge(self, challb: messages.ChallengeBody, + response: challenges.ChallengeResponse) -> messages.ChallengeResource: + """Answer challenge. + + :param challb: Challenge Resource body. + :type challb: `.ChallengeBody` + + :param response: Corresponding Challenge response + :type response: `.challenges.ChallengeResponse` + + :returns: Challenge Resource with updated body. + :rtype: `.ChallengeResource` + + :raises .UnexpectedUpdate: + + """ + resp = self._post(challb.uri, response) + try: + authzr_uri = resp.links['up']['url'] + except KeyError: + raise errors.ClientError('"up" Link header missing') + challr = messages.ChallengeResource( + authzr_uri=authzr_uri, + body=messages.ChallengeBody.from_json(resp.json())) + # TODO: check that challr.uri == resp.headers['Location']? + if challr.uri != challb.uri: + raise errors.UnexpectedUpdate(challr.uri) + return challr + + @classmethod + def retry_after(cls, response: requests.Response, default: int) -> datetime.datetime: + """Compute next `poll` time based on response ``Retry-After`` header. + + Handles integers and various datestring formats per + https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.37 + + :param requests.Response response: Response from `poll`. + :param int default: Default value (in seconds), used when + ``Retry-After`` header is not present or invalid. + + :returns: Time point when next `poll` should be performed. + :rtype: `datetime.datetime` + + """ + retry_after = response.headers.get('Retry-After', str(default)) + try: + seconds = int(retry_after) + except ValueError: + # The RFC 2822 parser handles all of RFC 2616's cases in modern + # environments (primarily HTTP 1.1+ but also py27+) + when = parsedate_tz(retry_after) + if when is not None: try: - chain = client_v1.fetch_chain(certr) - break - except errors.Error: - time.sleep(1) + tz_secs = datetime.timedelta(when[-1] if when[-1] is not None else 0) + return datetime.datetime(*when[:7]) - tz_secs + except (ValueError, OverflowError): + pass + seconds = default - if chain is None: - raise errors.TimeoutError( - 'Failed to fetch chain. You should not deploy the generated ' - 'certificate, please rerun the command for a new one.') + return datetime.datetime.now() + datetime.timedelta(seconds=seconds) - cert = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, - cast(OpenSSL.crypto.X509, cast(jose.ComparableX509, certr.body).wrapped)).decode() - chain_str = crypto_util.dump_pyopenssl_chain(chain).decode() - - return orderr.update(fullchain_pem=(cert + chain_str)) - return cast(ClientV2, self.client).finalize_order( - orderr, deadline, fetch_alternative_chains) - - def revoke(self, cert: jose.ComparableX509, rsn: int) -> None: + def _revoke(self, cert: jose.ComparableX509, rsn: int, url: str) -> None: """Revoke certificate. :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in @@ -992,23 +448,18 @@ class BackwardsCompatibleClientV2: :param int rsn: Reason code for certificate revocation. + :param str url: ACME URL to post to + :raises .ClientError: If revocation is unsuccessful. """ - self.client.revoke(cert, rsn) - - def _acme_version_from_directory(self, directory: messages.Directory) -> int: - if hasattr(directory, 'newNonce'): - return 2 - return 1 - - def external_account_required(self) -> bool: - """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 - return cast(ClientV2, self.client).external_account_required() + response = self._post(url, + messages.Revocation( + certificate=cert, + reason=rsn)) + if response.status_code != http_client.OK: + raise errors.ClientError( + 'Successful revocation must return HTTP OK status') class ClientNetwork: @@ -1025,8 +476,8 @@ class ClientNetwork: :param josepy.JWK key: Account private key :param messages.RegistrationResource account: Account object. Required if you are - planning to use .post() with acme_version=2 for anything other than - creating a new account; may be set later after registering. + planning to use .post() for anything other than creating a new account; + may be set later after registering. :param josepy.JWASignature alg: Algorithm to use in signing JWS. :param bool verify_ssl: Whether to verify certificates on SSL connections. :param str user_agent: String to send as User-Agent header. @@ -1062,8 +513,7 @@ class ClientNetwork: except Exception: # pylint: disable=broad-except pass - def _wrap_in_jws(self, obj: jose.JSONDeSerializable, nonce: str, url: str, - acme_version: int) -> str: + def _wrap_in_jws(self, obj: jose.JSONDeSerializable, nonce: str, url: str) -> str: """Wrap `JSONDeSerializable` object in JWS. .. todo:: Implement ``acmePath``. @@ -1074,20 +524,17 @@ class ClientNetwork: :rtype: str """ - if isinstance(obj, VersionedLEACMEMixin): - obj.le_acme_version = acme_version jobj = obj.json_dumps(indent=2).encode() if obj else b'' logger.debug('JWS payload:\n%s', jobj) kwargs = { "alg": self.alg, "nonce": nonce, + "url": url } - if acme_version == 2: - kwargs["url"] = url - # newAccount and revokeCert work without the kid - # newAccount must not have kid - if self.account is not None: - kwargs["kid"] = self.account["uri"] + # newAccount and revokeCert work without the kid + # newAccount must not have kid + if self.account is not None: + kwargs["kid"] = self.account["uri"] kwargs["key"] = self.key return jws.JWS.sign(jobj, **cast(Mapping[str, Any], kwargs)).json_dumps(indent=2) @@ -1201,15 +648,11 @@ class ClientNetwork: host, path, _err_no, err_msg = m.groups() raise ValueError(f"Requesting {host}{path}:{err_msg}") - # If the Content-Type is DER or an Accept header was sent in the - # request, the response may not be UTF-8 encoded. In this case, we - # don't set response.encoding and log the base64 response instead of - # raw bytes to keep binary data out of the logs. This code can be - # simplified to only check for an Accept header in the request when - # ACMEv1 support is dropped. + # If an Accept header was sent in the request, the response may not be + # UTF-8 encoded. In this case, we don't set response.encoding and log + # the base64 response instead of raw bytes to keep binary data out of the logs. debug_content: Union[bytes, str] - if (response.headers.get("Content-Type") == DER_CONTENT_TYPE or - "Accept" in kwargs["headers"]): + if "Accept" in kwargs["headers"]: debug_content = base64.b64encode(response.content) else: # We set response.encoding so response.text knows the response is @@ -1280,44 +723,11 @@ class ClientNetwork: raise def _post_once(self, url: str, obj: jose.JSONDeSerializable, - content_type: str = JOSE_CONTENT_TYPE, acme_version: int = 1, - **kwargs: Any) -> requests.Response: + content_type: str = JOSE_CONTENT_TYPE, **kwargs: Any) -> requests.Response: new_nonce_url = kwargs.pop('new_nonce_url', None) - data = self._wrap_in_jws(obj, self._get_nonce(url, new_nonce_url), url, acme_version) + data = self._wrap_in_jws(obj, self._get_nonce(url, new_nonce_url), url) kwargs.setdefault('headers', {'Content-Type': content_type}) response = self._send_request('POST', url, data=data, **kwargs) response = self._check_response(response, content_type=content_type) self._add_nonce(response) return response - - -# This class takes a similar approach to the cryptography project to deprecate attributes -# in public modules. See the _ModuleWithDeprecation class here: -# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 -class _ClientDeprecationModule: - """ - Internal class delegating to a module, and displaying warnings when attributes - related to deprecated attributes in the acme.client module. - """ - def __init__(self, module: ModuleType) -> None: - self.__dict__['_module'] = module - - def __getattr__(self, attr: str) -> Any: - if attr in ('Client', 'BackwardsCompatibleClientV2'): - warnings.warn('The {0} attribute in acme.client is deprecated ' - 'and will be removed soon.'.format(attr), - DeprecationWarning, stacklevel=2) - return getattr(self._module, attr) - - def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover - setattr(self._module, attr, value) - - def __delattr__(self, attr: str) -> None: # pragma: no cover - delattr(self._module, attr) - - def __dir__(self) -> List[str]: # pragma: no cover - return ['_module'] + dir(self._module) - - -# Patching ourselves to warn about deprecation and planned removal of some elements in the module. -sys.modules[__name__] = cast(ModuleType, _ClientDeprecationModule(sys.modules[__name__])) diff --git a/acme/acme/fields.py b/acme/acme/fields.py index 191231df2..d642d10c5 100644 --- a/acme/acme/fields.py +++ b/acme/acme/fields.py @@ -51,22 +51,6 @@ class RFC3339Field(jose.Field): raise jose.DeserializationError(error) -class Resource(jose.Field): - """Resource MITM field.""" - - def __init__(self, resource_type: str, *args: Any, **kwargs: Any) -> None: - self.resource_type = resource_type - kwargs['default'] = resource_type - super().__init__('resource', *args, **kwargs) - - def decode(self, value: Any) -> Any: - if value != self.resource_type: - raise jose.DeserializationError( - 'Wrong resource type: {0} instead of {1}'.format( - value, self.resource_type)) - return value - - def fixed(json_name: str, value: Any) -> Any: """Generates a type-friendly Fixed field.""" return Fixed(json_name, value) @@ -75,8 +59,3 @@ def fixed(json_name: str, value: Any) -> Any: def rfc3339(json_name: str, omitempty: bool = False) -> Any: """Generates a type-friendly RFC3339 field.""" return RFC3339Field(json_name, omitempty=omitempty) - - -def resource(resource_type: str) -> Any: - """Generates a type-friendly Resource field.""" - return Resource(resource_type) diff --git a/acme/acme/magic_typing.py b/acme/acme/magic_typing.py deleted file mode 100644 index b05d2c4bc..000000000 --- a/acme/acme/magic_typing.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Simple shim around the typing module. - -This was useful when this code supported Python 2 and typing wasn't always -available. This code is being kept for now for backwards compatibility. - -""" -import warnings -from typing import * # pylint: disable=wildcard-import, unused-wildcard-import -from typing import Any - -warnings.warn("acme.magic_typing is deprecated and will be removed in a future release.", - DeprecationWarning) - - -class TypingClass: - """Ignore import errors by getting anything""" - def __getattr__(self, name: str) -> Any: - return None # pragma: no cover diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 9b9ef5de2..6c4e23815 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -11,9 +11,7 @@ from typing import MutableMapping from typing import Optional from typing import Tuple from typing import Type -from typing import TYPE_CHECKING from typing import TypeVar -from typing import Union import josepy as jose @@ -22,14 +20,8 @@ from acme import errors from acme import fields from acme import jws from acme import util -from acme.mixins import ResourceMixin -if TYPE_CHECKING: - from typing_extensions import Protocol # pragma: no cover -else: - Protocol = object -OLD_ERROR_PREFIX = "urn:acme:error:" ERROR_PREFIX = "urn:ietf:params:acme:error:" ERROR_CODES = { @@ -67,15 +59,13 @@ ERROR_CODES = { ERROR_TYPE_DESCRIPTIONS = {**{ ERROR_PREFIX + name: desc for name, desc in ERROR_CODES.items() -}, **{ # add errors with old prefix, deprecate me - OLD_ERROR_PREFIX + name: desc for name, desc in ERROR_CODES.items() }} def is_acme_error(err: BaseException) -> bool: """Check if argument is an ACME error.""" if isinstance(err, Error) and (err.typ is not None): - return (ERROR_PREFIX in err.typ) or (OLD_ERROR_PREFIX in err.typ) + return ERROR_PREFIX in err.typ return False @@ -223,25 +213,15 @@ STATUS_READY = Status('ready') STATUS_DEACTIVATED = Status('deactivated') -class HasResourceType(Protocol): - """ - Represents a class with a resource_type class parameter of type string. - """ - resource_type: str = NotImplemented - - -GenericHasResourceType = TypeVar("GenericHasResourceType", bound=HasResourceType) - - class Directory(jose.JSONDeSerializable): - """Directory.""" + """Directory. - _REGISTERED_TYPES: Dict[str, Type[HasResourceType]] = {} + Directory resources must be accessed by the exact field name in RFC8555 (section 9.7.5). + """ class Meta(jose.JSONObjectWithFields): """Directory Meta.""" - _terms_of_service: str = jose.field('terms-of-service', omitempty=True) - _terms_of_service_v2: str = jose.field('termsOfService', omitempty=True) + _terms_of_service: str = jose.field('termsOfService', omitempty=True) website: str = jose.field('website', omitempty=True) caa_identities: List[str] = jose.field('caaIdentities', omitempty=True) external_account_required: bool = jose.field('externalAccountRequired', omitempty=True) @@ -253,7 +233,7 @@ class Directory(jose.JSONDeSerializable): @property def terms_of_service(self) -> str: """URL for the CA TOS""" - return self._terms_of_service or self._terms_of_service_v2 + return self._terms_of_service def __iter__(self) -> Iterator[str]: # When iterating over fields, use the external name 'terms_of_service' instead of @@ -264,41 +244,23 @@ class Directory(jose.JSONDeSerializable): def _internal_name(self, name: str) -> str: return '_' + name if name == 'terms_of_service' else name - @classmethod - def _canon_key(cls, key: Union[str, HasResourceType, Type[HasResourceType]]) -> str: - if isinstance(key, str): - return key - return key.resource_type - - @classmethod - def register(cls, - resource_body_cls: Type[GenericHasResourceType]) -> Type[GenericHasResourceType]: - """Register resource.""" - resource_type = resource_body_cls.resource_type - assert resource_type not in cls._REGISTERED_TYPES - cls._REGISTERED_TYPES[resource_type] = resource_body_cls - return resource_body_cls - def __init__(self, jobj: Mapping[str, Any]) -> None: - canon_jobj = util.map_keys(jobj, self._canon_key) - # TODO: check that everything is an absolute URL; acme-spec is - # not clear on that - self._jobj = canon_jobj + self._jobj = jobj def __getattr__(self, name: str) -> Any: try: - return self[name.replace('_', '-')] + return self[name] except KeyError as error: raise AttributeError(str(error)) - def __getitem__(self, name: Union[str, HasResourceType, Type[HasResourceType]]) -> Any: + def __getitem__(self, name: str) -> Any: try: - return self._jobj[self._canon_key(name)] + return self._jobj[name] except KeyError: - raise KeyError('Directory field "' + self._canon_key(name) + '" not found') + raise KeyError(f'Directory field "{name}" not found') def to_partial_json(self) -> Dict[str, Any]: - return self._jobj + return util.map_keys(self._jobj, lambda k: k) @classmethod def from_json(cls, jobj: MutableMapping[str, Any]) -> 'Directory': @@ -459,17 +421,12 @@ class Registration(ResourceBody): return self._filter_contact(self.email_prefix) -@Directory.register -class NewRegistration(ResourceMixin, Registration): +class NewRegistration(Registration): """New registration.""" - resource_type = 'new-reg' - resource: str = fields.resource(resource_type) -class UpdateRegistration(ResourceMixin, Registration): +class UpdateRegistration(Registration): """Update registration.""" - resource_type = 'reg' - resource: str = fields.resource(resource_type) class RegistrationResource(ResourceWithURI): @@ -507,7 +464,6 @@ class ChallengeBody(ResourceBody): # challenge object supports either one, but should be accessed through the # name "uri". In Client.answer_challenge, whichever one is set will be # used. - _uri: str = jose.field('uri', omitempty=True, default=None) _url: str = jose.field('url', omitempty=True, default=None) status: Status = jose.field('status', decoder=Status.from_json, omitempty=True, default=STATUS_PENDING) @@ -536,7 +492,7 @@ class ChallengeBody(ResourceBody): @property def uri(self) -> str: """The URL of this challenge.""" - return self._url or self._uri + return self._url def __getattr__(self, name: str) -> Any: return getattr(self.chall, name) @@ -545,10 +501,10 @@ class ChallengeBody(ResourceBody): # When iterating over fields, use the external name 'uri' instead of # the internal '_uri'. for name in super().__iter__(): - yield name[1:] if name == '_uri' else name + yield 'uri' if name == '_url' else name def _internal_name(self, name: str) -> str: - return '_' + name if name == 'uri' else name + return '_url' if name == 'uri' else name class ChallengeResource(Resource): @@ -572,15 +528,12 @@ class Authorization(ResourceBody): :ivar acme.messages.Identifier identifier: :ivar list challenges: `list` of `.ChallengeBody` - :ivar tuple combinations: Challenge combinations (`tuple` of `tuple` - of `int`, as opposed to `list` of `list` from the spec). :ivar acme.messages.Status status: :ivar datetime.datetime expires: """ identifier: Identifier = jose.field('identifier', decoder=Identifier.from_json, omitempty=True) challenges: List[ChallengeBody] = jose.field('challenges', omitempty=True) - combinations: Tuple[Tuple[int, ...], ...] = jose.field('combinations', omitempty=True) status: Status = jose.field('status', omitempty=True, decoder=Status.from_json) # TODO: 'expires' is allowed for Authorization Resources in @@ -596,24 +549,13 @@ class Authorization(ResourceBody): def challenges(value: List[Dict[str, Any]]) -> Tuple[ChallengeBody, ...]: # type: ignore[misc] # pylint: disable=no-self-argument,missing-function-docstring return tuple(ChallengeBody.from_json(chall) for chall in value) - @property - def resolved_combinations(self) -> Tuple[Tuple[ChallengeBody, ...], ...]: - """Combinations with challenges instead of indices.""" - return tuple(tuple(self.challenges[idx] for idx in combo) - for combo in self.combinations) # pylint: disable=not-an-iterable - -@Directory.register -class NewAuthorization(ResourceMixin, Authorization): +class NewAuthorization(Authorization): """New authorization.""" - resource_type = 'new-authz' - resource: str = fields.resource(resource_type) -class UpdateAuthorization(ResourceMixin, Authorization): +class UpdateAuthorization(Authorization): """Update authorization.""" - resource_type = 'authz' - resource: str = fields.resource(resource_type) class AuthorizationResource(ResourceWithURI): @@ -627,16 +569,13 @@ class AuthorizationResource(ResourceWithURI): new_cert_uri: str = jose.field('new_cert_uri', omitempty=True) -@Directory.register -class CertificateRequest(ResourceMixin, jose.JSONObjectWithFields): - """ACME new-cert request. +class CertificateRequest(jose.JSONObjectWithFields): + """ACME newOrder request. :ivar jose.ComparableX509 csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` """ - resource_type = 'new-cert' - resource: str = fields.resource(resource_type) csr: jose.ComparableX509 = jose.field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) @@ -653,16 +592,13 @@ class CertificateResource(ResourceWithURI): authzrs: Tuple[AuthorizationResource, ...] = jose.field('authzrs') -@Directory.register -class Revocation(ResourceMixin, jose.JSONObjectWithFields): +class Revocation(jose.JSONObjectWithFields): """Revocation message. :ivar jose.ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in `jose.ComparableX509` """ - resource_type = 'revoke-cert' - resource: str = fields.resource(resource_type) certificate: jose.ComparableX509 = jose.field( 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) reason: int = jose.field('reason') @@ -719,7 +655,5 @@ class OrderResource(ResourceWithURI): omitempty=True) -@Directory.register class NewOrder(Order): """New order.""" - resource_type = 'new-order' diff --git a/acme/acme/mixins.py b/acme/acme/mixins.py deleted file mode 100644 index e6e678d60..000000000 --- a/acme/acme/mixins.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Useful mixins for Challenge and Resource objects""" -from typing import Any -from typing import Dict - - -class VersionedLEACMEMixin: - """This mixin stores the version of Let's Encrypt's endpoint being used.""" - @property - def le_acme_version(self) -> int: - """Define the version of ACME protocol to use""" - return getattr(self, '_le_acme_version', 1) - - @le_acme_version.setter - def le_acme_version(self, version: int) -> None: - # We need to use object.__setattr__ to not depend on the specific implementation of - # __setattr__ in current class (eg. jose.TypedJSONObjectWithFields raises AttributeError - # for any attempt to set an attribute to make objects immutable). - object.__setattr__(self, '_le_acme_version', version) - - def __setattr__(self, key: str, value: Any) -> None: - if key == 'le_acme_version': - # Required for @property to operate properly. See comment above. - object.__setattr__(self, key, value) - else: - super().__setattr__(key, value) # pragma: no cover - - -class ResourceMixin(VersionedLEACMEMixin): - """ - This mixin generates a RFC8555 compliant JWS payload - by removing the `resource` field if needed (eg. ACME v2 protocol). - """ - def to_partial_json(self) -> Dict[str, Any]: - """See josepy.JSONDeserializable.to_partial_json()""" - return _safe_jobj_compliance(super(), - 'to_partial_json', 'resource') - - def fields_to_partial_json(self) -> Dict[str, Any]: - """See josepy.JSONObjectWithFields.fields_to_partial_json()""" - return _safe_jobj_compliance(super(), - 'fields_to_partial_json', 'resource') - - -class TypeMixin(VersionedLEACMEMixin): - """ - This mixin allows generation of a RFC8555 compliant JWS payload - by removing the `type` field if needed (eg. ACME v2 protocol). - """ - def to_partial_json(self) -> Dict[str, Any]: - """See josepy.JSONDeserializable.to_partial_json()""" - return _safe_jobj_compliance(super(), - 'to_partial_json', 'type') - - def fields_to_partial_json(self) -> Dict[str, Any]: - """See josepy.JSONObjectWithFields.fields_to_partial_json()""" - return _safe_jobj_compliance(super(), - 'fields_to_partial_json', 'type') - - -def _safe_jobj_compliance(instance: Any, jobj_method: str, - uncompliant_field: str) -> Dict[str, Any]: - if hasattr(instance, jobj_method): - jobj: Dict[str, Any] = getattr(instance, jobj_method)() - if instance.le_acme_version == 2: - jobj.pop(uncompliant_field, None) - return jobj - - raise AttributeError(f'Method {jobj_method}() is not implemented.') # pragma: no cover diff --git a/acme/examples/http01_example.py b/acme/examples/http01_example.py index 2dc197d09..ab62ecbcc 100644 --- a/acme/examples/http01_example.py +++ b/acme/examples/http01_example.py @@ -163,7 +163,7 @@ def example_http(): # Register account and accept TOS net = client.ClientNetwork(acc_key, user_agent=USER_AGENT) - directory = messages.Directory.from_json(net.get(DIRECTORY_URL).json()) + directory = client.ClientV2.get_directory(DIRECTORY_URL, net) client_acme = client.ClientV2(directory, net=net) # Terms of Service URL is in client_acme.directory.meta.terms_of_service @@ -215,8 +215,7 @@ def example_http(): try: regr = client_acme.query_registration(regr) except errors.Error as err: - if err.typ == messages.OLD_ERROR_PREFIX + 'unauthorized' \ - or err.typ == messages.ERROR_PREFIX + 'unauthorized': + if err.typ == messages.ERROR_PREFIX + 'unauthorized': # Status is deactivated. pass raise diff --git a/acme/tests/challenges_test.py b/acme/tests/challenges_test.py index d7815a6c3..f9e886f64 100644 --- a/acme/tests/challenges_test.py +++ b/acme/tests/challenges_test.py @@ -92,8 +92,7 @@ class DNS01ResponseTest(unittest.TestCase): self.response = self.chall.response(KEY) def test_to_partial_json(self): - self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, - self.msg.to_partial_json()) + self.assertEqual({}, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import DNS01Response @@ -163,8 +162,7 @@ class HTTP01ResponseTest(unittest.TestCase): self.response = self.chall.response(KEY) def test_to_partial_json(self): - self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, - self.msg.to_partial_json()) + self.assertEqual({}, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import HTTP01Response @@ -274,8 +272,7 @@ class TLSALPN01ResponseTest(unittest.TestCase): } def test_to_partial_json(self): - self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, - self.response.to_partial_json()) + self.assertEqual({}, self.response.to_partial_json()) def test_from_json(self): from acme.challenges import TLSALPN01Response @@ -461,8 +458,6 @@ class DNSResponseTest(unittest.TestCase): from acme.challenges import DNSResponse self.msg = DNSResponse(validation=self.validation) self.jmsg_to = { - 'resource': 'challenge', - 'type': 'dns', 'validation': self.validation, } self.jmsg_from = { @@ -492,7 +487,6 @@ class JWSPayloadRFC8555Compliant(unittest.TestCase): from acme.challenges import HTTP01Response challenge_body = HTTP01Response() - challenge_body.le_acme_version = 2 jobj = challenge_body.json_dumps(indent=2).encode() # RFC8555 states that challenge responses must have an empty payload. diff --git a/acme/tests/client_test.py b/acme/tests/client_test.py index 7ce28b4fe..1b00e0b90 100644 --- a/acme/tests/client_test.py +++ b/acme/tests/client_test.py @@ -3,52 +3,37 @@ import copy import datetime import http.client as http_client -import ipaddress import json import unittest from typing import Dict from unittest import mock import josepy as jose -import OpenSSL import requests from acme import challenges from acme import errors from acme import jws as acme_jws from acme import messages -from acme.mixins import VersionedLEACMEMixin +from acme.client import ClientV2 import messages_test import test_util -CERT_DER = test_util.load_vector('cert.der') CERT_SAN_PEM = test_util.load_vector('cert-san.pem') -CSR_SAN_PEM = test_util.load_vector('csr-san.pem') CSR_MIXED_PEM = test_util.load_vector('csr-mixed.pem') KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) -KEY2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) - -DIRECTORY_V1 = messages.Directory({ - messages.NewRegistration: - 'https://www.letsencrypt-demo.org/acme/new-reg', - messages.Revocation: - 'https://www.letsencrypt-demo.org/acme/revoke-cert', - messages.NewAuthorization: - 'https://www.letsencrypt-demo.org/acme/new-authz', - messages.CertificateRequest: - 'https://www.letsencrypt-demo.org/acme/new-cert', -}) DIRECTORY_V2 = messages.Directory({ 'newAccount': 'https://www.letsencrypt-demo.org/acme/new-account', 'newNonce': 'https://www.letsencrypt-demo.org/acme/new-nonce', 'newOrder': 'https://www.letsencrypt-demo.org/acme/new-order', 'revokeCert': 'https://www.letsencrypt-demo.org/acme/revoke-cert', + 'meta': messages.Directory.Meta(), }) -class ClientTestBase(unittest.TestCase): - """Base for tests in acme.client.""" +class ClientV2Test(unittest.TestCase): + """Tests for acme.client.ClientV2.""" def setUp(self): self.response = mock.MagicMock( @@ -80,650 +65,15 @@ class ClientTestBase(unittest.TestCase): self.authz = messages.Authorization( identifier=messages.Identifier( typ=messages.IDENTIFIER_FQDN, value='example.com'), - challenges=(challb,), combinations=None) + challenges=(challb,)) self.authzr = messages.AuthorizationResource( body=self.authz, uri=authzr_uri) # Reason code for revocation self.rsn = 1 - -class BackwardsCompatibleClientV2Test(ClientTestBase): - """Tests for acme.client.BackwardsCompatibleClientV2.""" - - def setUp(self): - super().setUp() - # contains a loaded cert - self.certr = messages.CertificateResource( - body=messages_test.CERT) - - loaded = OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, CERT_SAN_PEM) - wrapped = jose.ComparableX509(loaded) - self.chain = [wrapped, wrapped] - - self.cert_pem = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, messages_test.CERT.wrapped).decode() - - single_chain = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, loaded).decode() - self.chain_pem = single_chain + single_chain - - self.fullchain_pem = self.cert_pem + self.chain_pem - - self.orderr = messages.OrderResource( - csr_pem=CSR_SAN_PEM) - - def _init(self): - uri = 'http://www.letsencrypt-demo.org/directory' - from acme.client import BackwardsCompatibleClientV2 - return BackwardsCompatibleClientV2(net=self.net, - key=KEY, server=uri) - - def test_init_downloads_directory(self): - uri = 'http://www.letsencrypt-demo.org/directory' - from acme.client import BackwardsCompatibleClientV2 - BackwardsCompatibleClientV2(net=self.net, - key=KEY, server=uri) - self.net.get.assert_called_once_with(uri) - - def test_init_acme_version(self): - self.response.json.return_value = DIRECTORY_V1.to_json() - client = self._init() - self.assertEqual(client.acme_version, 1) - - self.response.json.return_value = DIRECTORY_V2.to_json() - client = self._init() - self.assertEqual(client.acme_version, 2) - - def test_query_registration_client_v2(self): - self.response.json.return_value = DIRECTORY_V2.to_json() - client = self._init() - self.response.json.return_value = self.regr.body.to_json() - self.response.headers = {'Location': 'https://www.letsencrypt-demo.org/acme/reg/1'} - self.assertEqual(self.regr, client.query_registration(self.regr)) - - def test_forwarding(self): - self.response.json.return_value = DIRECTORY_V1.to_json() - client = self._init() - self.assertEqual(client.directory, client.client.directory) - self.assertEqual(client.key, KEY) - self.assertEqual(client.deactivate_registration, client.client.deactivate_registration) - self.assertRaises(AttributeError, client.__getattr__, 'nonexistent') - self.assertRaises(AttributeError, client.__getattr__, 'new_account_and_tos') - self.assertRaises(AttributeError, client.__getattr__, 'new_account') - - def test_new_account_and_tos(self): - # v2 no tos - self.response.json.return_value = DIRECTORY_V2.to_json() - with mock.patch('acme.client.ClientV2') as mock_client: - client = self._init() - client.new_account_and_tos(self.new_reg) - mock_client().new_account.assert_called_with(self.new_reg) - - # v2 tos good - with mock.patch('acme.client.ClientV2') as mock_client: - mock_client().directory.meta.__contains__.return_value = True - client = self._init() - client.new_account_and_tos(self.new_reg, lambda x: True) - mock_client().new_account.assert_called_with( - self.new_reg.update(terms_of_service_agreed=True)) - - # v2 tos bad - with mock.patch('acme.client.ClientV2') as mock_client: - mock_client().directory.meta.__contains__.return_value = True - client = self._init() - def _tos_cb(tos): - raise errors.Error - self.assertRaises(errors.Error, client.new_account_and_tos, - self.new_reg, _tos_cb) - mock_client().new_account.assert_not_called() - - # v1 yes tos - self.response.json.return_value = DIRECTORY_V1.to_json() - with mock.patch('acme.client.Client') as mock_client: - regr = mock.MagicMock(terms_of_service="TOS") - mock_client().register.return_value = regr - client = self._init() - client.new_account_and_tos(self.new_reg) - mock_client().register.assert_called_once_with(self.new_reg) - mock_client().agree_to_tos.assert_called_once_with(regr) - - # v1 no tos - with mock.patch('acme.client.Client') as mock_client: - regr = mock.MagicMock(terms_of_service=None) - mock_client().register.return_value = regr - client = self._init() - client.new_account_and_tos(self.new_reg) - mock_client().register.assert_called_once_with(self.new_reg) - mock_client().agree_to_tos.assert_not_called() - - @mock.patch('OpenSSL.crypto.load_certificate_request') - @mock.patch('acme.crypto_util._pyopenssl_cert_or_req_all_names') - def test_new_order_v1(self, mock__pyopenssl_cert_or_req_all_names, - unused_mock_load_certificate_request): - self.response.json.return_value = DIRECTORY_V1.to_json() - mock__pyopenssl_cert_or_req_all_names.return_value = ['example.com', 'www.example.com'] - mock_csr_pem = mock.MagicMock() - with mock.patch('acme.client.Client') as mock_client: - mock_client().request_domain_challenges.return_value = mock.sentinel.auth - client = self._init() - orderr = client.new_order(mock_csr_pem) - self.assertEqual(orderr.authorizations, [mock.sentinel.auth, mock.sentinel.auth]) - - def test_new_order_v2(self): - self.response.json.return_value = DIRECTORY_V2.to_json() - mock_csr_pem = mock.MagicMock() - with mock.patch('acme.client.ClientV2') as mock_client: - client = self._init() - client.new_order(mock_csr_pem) - mock_client().new_order.assert_called_once_with(mock_csr_pem) - - @mock.patch('acme.client.Client') - def test_finalize_order_v1_success(self, mock_client): - self.response.json.return_value = DIRECTORY_V1.to_json() - - mock_client().request_issuance.return_value = self.certr - mock_client().fetch_chain.return_value = self.chain - - deadline = datetime.datetime(9999, 9, 9) - client = self._init() - result = client.finalize_order(self.orderr, deadline) - self.assertEqual(result.fullchain_pem, self.fullchain_pem) - mock_client().fetch_chain.assert_called_once_with(self.certr) - - @mock.patch('acme.client.Client') - def test_finalize_order_v1_fetch_chain_error(self, mock_client): - self.response.json.return_value = DIRECTORY_V1.to_json() - - mock_client().request_issuance.return_value = self.certr - mock_client().fetch_chain.return_value = self.chain - mock_client().fetch_chain.side_effect = [errors.Error, self.chain] - - deadline = datetime.datetime(9999, 9, 9) - client = self._init() - result = client.finalize_order(self.orderr, deadline) - self.assertEqual(result.fullchain_pem, self.fullchain_pem) - self.assertEqual(mock_client().fetch_chain.call_count, 2) - - @mock.patch('acme.client.Client') - def test_finalize_order_v1_timeout(self, mock_client): - self.response.json.return_value = DIRECTORY_V1.to_json() - - mock_client().request_issuance.return_value = self.certr - - deadline = deadline = datetime.datetime.now() - datetime.timedelta(seconds=60) - client = self._init() - self.assertRaises(errors.TimeoutError, client.finalize_order, - self.orderr, deadline) - - def test_finalize_order_v2(self): - self.response.json.return_value = DIRECTORY_V2.to_json() - mock_orderr = mock.MagicMock() - mock_deadline = mock.MagicMock() - with mock.patch('acme.client.ClientV2') as mock_client: - client = self._init() - client.finalize_order(mock_orderr, mock_deadline) - mock_client().finalize_order.assert_called_once_with(mock_orderr, mock_deadline, False) - - def test_revoke(self): - self.response.json.return_value = DIRECTORY_V1.to_json() - with mock.patch('acme.client.Client') as mock_client: - client = self._init() - client.revoke(messages_test.CERT, self.rsn) - mock_client().revoke.assert_called_once_with(messages_test.CERT, self.rsn) - - self.response.json.return_value = DIRECTORY_V2.to_json() - with mock.patch('acme.client.ClientV2') as mock_client: - client = self._init() - client.revoke(messages_test.CERT, self.rsn) - mock_client().revoke.assert_called_once_with(messages_test.CERT, self.rsn) - - def test_update_registration(self): - self.response.json.return_value = DIRECTORY_V1.to_json() - with mock.patch('acme.client.Client') as mock_client: - client = self._init() - 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.""" - - def setUp(self): - super().setUp() - - self.directory = DIRECTORY_V1 - - # Registration - self.regr = self.regr.update( - terms_of_service='https://www.letsencrypt-demo.org/tos') - - # Request issuance - self.certr = messages.CertificateResource( - body=messages_test.CERT, authzrs=(self.authzr,), - uri='https://www.letsencrypt-demo.org/acme/cert/1', - cert_chain_uri='https://www.letsencrypt-demo.org/ca') - - from acme.client import Client - self.client = Client( - directory=self.directory, key=KEY, alg=jose.RS256, net=self.net) - - def test_init_downloads_directory(self): - uri = 'http://www.letsencrypt-demo.org/directory' - from acme.client import Client - self.client = Client( - directory=uri, key=KEY, alg=jose.RS256, net=self.net) - self.net.get.assert_called_once_with(uri) - - @mock.patch('acme.client.ClientNetwork') - def test_init_without_net(self, mock_net): - mock_net.return_value = mock.sentinel.net - alg = jose.RS256 - from acme.client import Client - self.client = Client( - directory=self.directory, key=KEY, alg=alg) - mock_net.called_once_with(KEY, alg=alg, verify_ssl=True) - self.assertEqual(self.client.net, mock.sentinel.net) - - def test_register(self): - # "Instance of 'Field' has no to_json/update member" bug: - self.response.status_code = http_client.CREATED - self.response.json.return_value = self.regr.body.to_json() - self.response.headers['Location'] = self.regr.uri - self.response.links.update({ - 'terms-of-service': {'url': self.regr.terms_of_service}, - }) - - self.assertEqual(self.regr, self.client.register(self.new_reg)) - # TODO: test POST call arguments - - def test_update_registration(self): - # "Instance of 'Field' has no to_json/update member" bug: - self.response.headers['Location'] = self.regr.uri - self.response.json.return_value = self.regr.body.to_json() - self.assertEqual(self.regr, self.client.update_registration(self.regr)) - # TODO: test POST call arguments - - # TODO: split here and separate test - self.response.json.return_value = self.regr.body.update( - contact=()).to_json() - - def test_deactivate_account(self): - self.response.headers['Location'] = self.regr.uri - self.response.json.return_value = self.regr.body.to_json() - self.assertEqual(self.regr, - self.client.deactivate_registration(self.regr)) - - def test_query_registration(self): - self.response.json.return_value = self.regr.body.to_json() - self.assertEqual(self.regr, self.client.query_registration(self.regr)) - - def test_agree_to_tos(self): - self.client.update_registration = mock.Mock() - self.client.agree_to_tos(self.regr) - regr = self.client.update_registration.call_args[0][0] - self.assertEqual(self.regr.terms_of_service, regr.body.agreement) - - def _prepare_response_for_request_challenges(self): - self.response.status_code = http_client.CREATED - self.response.headers['Location'] = self.authzr.uri - self.response.json.return_value = self.authz.to_json() - - def test_request_challenges(self): - self._prepare_response_for_request_challenges() - self.client.request_challenges(self.identifier) - self.net.post.assert_called_once_with( - self.directory.new_authz, - messages.NewAuthorization(identifier=self.identifier), - acme_version=1) - - def test_request_challenges_deprecated_arg(self): - self._prepare_response_for_request_challenges() - self.client.request_challenges(self.identifier, new_authzr_uri="hi") - self.net.post.assert_called_once_with( - self.directory.new_authz, - messages.NewAuthorization(identifier=self.identifier), - acme_version=1) - - def test_request_challenges_custom_uri(self): - self._prepare_response_for_request_challenges() - self.client.request_challenges(self.identifier) - self.net.post.assert_called_once_with( - 'https://www.letsencrypt-demo.org/acme/new-authz', mock.ANY, - acme_version=1) - - def test_request_challenges_unexpected_update(self): - self._prepare_response_for_request_challenges() - self.response.json.return_value = self.authz.update( - identifier=self.identifier.update(value='foo')).to_json() - self.assertRaises( - errors.UnexpectedUpdate, self.client.request_challenges, - self.identifier) - - def test_request_challenges_wildcard(self): - wildcard_identifier = messages.Identifier( - typ=messages.IDENTIFIER_FQDN, value='*.example.org') - self.assertRaises( - errors.WildcardUnsupportedError, self.client.request_challenges, - wildcard_identifier) - - def test_request_domain_challenges(self): - self.client.request_challenges = mock.MagicMock() - self.assertEqual( - self.client.request_challenges(self.identifier), - self.client.request_domain_challenges('example.com')) - - def test_answer_challenge(self): - self.response.links['up'] = {'url': self.challr.authzr_uri} - self.response.json.return_value = self.challr.body.to_json() - - chall_response = challenges.DNSResponse(validation=None) - - self.client.answer_challenge(self.challr.body, chall_response) - - # TODO: split here and separate test - self.assertRaises(errors.UnexpectedUpdate, self.client.answer_challenge, - self.challr.body.update(uri='foo'), chall_response) - - def test_answer_challenge_missing_next(self): - self.assertRaises( - errors.ClientError, self.client.answer_challenge, - self.challr.body, challenges.DNSResponse(validation=None)) - - def test_retry_after_date(self): - self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' - self.assertEqual( - datetime.datetime(1999, 12, 31, 23, 59, 59), - self.client.retry_after(response=self.response, default=10)) - - @mock.patch('acme.client.datetime') - def test_retry_after_invalid(self, dt_mock): - dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) - dt_mock.timedelta = datetime.timedelta - - self.response.headers['Retry-After'] = 'foooo' - self.assertEqual( - datetime.datetime(2015, 3, 27, 0, 0, 10), - self.client.retry_after(response=self.response, default=10)) - - @mock.patch('acme.client.datetime') - def test_retry_after_overflow(self, dt_mock): - dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) - dt_mock.timedelta = datetime.timedelta - dt_mock.datetime.side_effect = datetime.datetime - - self.response.headers['Retry-After'] = "Tue, 116 Feb 2016 11:50:00 MST" - self.assertEqual( - datetime.datetime(2015, 3, 27, 0, 0, 10), - self.client.retry_after(response=self.response, default=10)) - - @mock.patch('acme.client.datetime') - def test_retry_after_seconds(self, dt_mock): - dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) - dt_mock.timedelta = datetime.timedelta - - self.response.headers['Retry-After'] = '50' - self.assertEqual( - datetime.datetime(2015, 3, 27, 0, 0, 50), - self.client.retry_after(response=self.response, default=10)) - - @mock.patch('acme.client.datetime') - def test_retry_after_missing(self, dt_mock): - dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) - dt_mock.timedelta = datetime.timedelta - - self.assertEqual( - datetime.datetime(2015, 3, 27, 0, 0, 10), - self.client.retry_after(response=self.response, default=10)) - - def test_poll(self): - self.response.json.return_value = self.authzr.body.to_json() - self.assertEqual((self.authzr, self.response), - self.client.poll(self.authzr)) - - # TODO: split here and separate test - self.response.json.return_value = self.authz.update( - identifier=self.identifier.update(value='foo')).to_json() - self.assertRaises( - errors.UnexpectedUpdate, self.client.poll, self.authzr) - - def test_request_issuance(self): - self.response.content = CERT_DER - self.response.headers['Location'] = self.certr.uri - self.response.links['up'] = {'url': self.certr.cert_chain_uri} - self.assertEqual(self.certr, self.client.request_issuance( - messages_test.CSR, (self.authzr,))) - # TODO: check POST args - - def test_request_issuance_missing_up(self): - self.response.content = CERT_DER - self.response.headers['Location'] = self.certr.uri - self.assertEqual( - self.certr.update(cert_chain_uri=None), - self.client.request_issuance(messages_test.CSR, (self.authzr,))) - - def test_request_issuance_missing_location(self): - self.assertRaises( - errors.ClientError, self.client.request_issuance, - messages_test.CSR, (self.authzr,)) - - @mock.patch('acme.client.datetime') - @mock.patch('acme.client.time') - def test_poll_and_request_issuance(self, time_mock, dt_mock): - # clock.dt | pylint: disable=no-member - clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27)) - - def sleep(seconds): - """increment clock""" - clock.dt += datetime.timedelta(seconds=seconds) - time_mock.sleep.side_effect = sleep - - def now(): - """return current clock value""" - return clock.dt - dt_mock.datetime.now.side_effect = now - dt_mock.timedelta = datetime.timedelta - - def poll(authzr): # pylint: disable=missing-docstring - # record poll start time based on the current clock value - authzr.times.append(clock.dt) - - # suppose it takes 2 seconds for server to produce the - # result, increment clock - clock.dt += datetime.timedelta(seconds=2) - - if len(authzr.retries) == 1: # no more retries - done = mock.MagicMock(uri=authzr.uri, times=authzr.times) - done.body.status = authzr.retries[0] - return done, [] - - # response (2nd result tuple element) is reduced to only - # Retry-After header contents represented as integer - # seconds; authzr.retries is a list of Retry-After - # headers, head(retries) is peeled of as a current - # Retry-After header, and tail(retries) is persisted for - # later poll() calls - return (mock.MagicMock(retries=authzr.retries[1:], - uri=authzr.uri + '.', times=authzr.times), - authzr.retries[0]) - self.client.poll = mock.MagicMock(side_effect=poll) - - mintime = 7 - - def retry_after(response, default): - # pylint: disable=missing-docstring - # check that poll_and_request_issuance correctly passes mintime - self.assertEqual(default, mintime) - return clock.dt + datetime.timedelta(seconds=response) - self.client.retry_after = mock.MagicMock(side_effect=retry_after) - - def request_issuance(csr, authzrs): # pylint: disable=missing-docstring - return csr, authzrs - self.client.request_issuance = mock.MagicMock( - side_effect=request_issuance) - - csr = mock.MagicMock() - authzrs = ( - mock.MagicMock(uri='a', times=[], retries=( - 8, 20, 30, messages.STATUS_VALID)), - mock.MagicMock(uri='b', times=[], retries=( - 5, messages.STATUS_VALID)), - ) - - cert, updated_authzrs = self.client.poll_and_request_issuance( - csr, authzrs, mintime=mintime, - # make sure that max_attempts is per-authorization, rather - # than global - max_attempts=max(len(authzrs[0].retries), len(authzrs[1].retries))) - self.assertIs(cert[0], csr) - self.assertIs(cert[1], updated_authzrs) - self.assertEqual(updated_authzrs[0].uri, 'a...') - self.assertEqual(updated_authzrs[1].uri, 'b.') - self.assertEqual(updated_authzrs[0].times, [ - datetime.datetime(2015, 3, 27), - # a is scheduled for 10, but b is polling [9..11), so it - # will be picked up as soon as b is finished, without - # additional sleeping - datetime.datetime(2015, 3, 27, 0, 0, 11), - datetime.datetime(2015, 3, 27, 0, 0, 33), - datetime.datetime(2015, 3, 27, 0, 1, 5), - ]) - self.assertEqual(updated_authzrs[1].times, [ - datetime.datetime(2015, 3, 27, 0, 0, 2), - datetime.datetime(2015, 3, 27, 0, 0, 9), - ]) - self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7)) - - # CA sets invalid | TODO: move to a separate test - invalid_authzr = mock.MagicMock( - times=[], retries=[messages.STATUS_INVALID]) - self.assertRaises( - errors.PollError, self.client.poll_and_request_issuance, - csr, authzrs=(invalid_authzr,), mintime=mintime) - - # exceeded max_attempts | TODO: move to a separate test - self.assertRaises( - errors.PollError, self.client.poll_and_request_issuance, - csr, authzrs, mintime=mintime, max_attempts=2) - - def test_deactivate_authorization(self): - authzb = self.authzr.body.update(status=messages.STATUS_DEACTIVATED) - self.response.json.return_value = authzb.to_json() - authzr = self.client.deactivate_authorization(self.authzr) - self.assertEqual(authzb, authzr.body) - self.assertEqual(self.client.net.post.call_count, 1) - self.assertIn(self.authzr.uri, self.net.post.call_args_list[0][0]) - - def test_check_cert(self): - self.response.headers['Location'] = self.certr.uri - self.response.content = CERT_DER - self.assertEqual(self.certr.update(body=messages_test.CERT), - self.client.check_cert(self.certr)) - - # TODO: split here and separate test - self.response.headers['Location'] = 'foo' - self.assertRaises( - errors.UnexpectedUpdate, self.client.check_cert, self.certr) - - def test_check_cert_missing_location(self): - self.response.content = CERT_DER - self.assertRaises( - errors.ClientError, self.client.check_cert, self.certr) - - def test_refresh(self): - self.client.check_cert = mock.MagicMock() - self.assertEqual( - self.client.check_cert(self.certr), self.client.refresh(self.certr)) - - def test_fetch_chain_no_up_link(self): - self.assertEqual([], self.client.fetch_chain(self.certr.update( - cert_chain_uri=None))) - - def test_fetch_chain_single(self): - # pylint: disable=protected-access - self.client._get_cert = mock.MagicMock() - self.client._get_cert.return_value = ( - mock.MagicMock(links={}), "certificate") - self.assertEqual([self.client._get_cert(self.certr.cert_chain_uri)[1]], - self.client.fetch_chain(self.certr)) - - def test_fetch_chain_max(self): - # pylint: disable=protected-access - up_response = mock.MagicMock(links={'up': {'url': 'http://cert'}}) - noup_response = mock.MagicMock(links={}) - self.client._get_cert = mock.MagicMock() - self.client._get_cert.side_effect = [ - (up_response, "cert")] * 9 + [(noup_response, "last_cert")] - chain = self.client.fetch_chain(self.certr, max_length=10) - self.assertEqual(chain, ["cert"] * 9 + ["last_cert"]) - - def test_fetch_chain_too_many(self): # recursive - # pylint: disable=protected-access - response = mock.MagicMock(links={'up': {'url': 'http://cert'}}) - self.client._get_cert = mock.MagicMock() - self.client._get_cert.return_value = (response, "certificate") - self.assertRaises(errors.Error, self.client.fetch_chain, self.certr) - - def test_revoke(self): - self.client.revoke(self.certr.body, self.rsn) - self.net.post.assert_called_once_with( - self.directory[messages.Revocation], mock.ANY, acme_version=1) - - def test_revocation_payload(self): - obj = messages.Revocation(certificate=self.certr.body, reason=self.rsn) - self.assertIn('reason', obj.to_partial_json().keys()) - 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 - self.assertRaises( - errors.ClientError, - self.client.revoke, - self.certr, - self.rsn) - - -class ClientV2Test(ClientTestBase): - """Tests for acme.client.ClientV2.""" - - def setUp(self): - super().setUp() - self.directory = DIRECTORY_V2 - from acme.client import ClientV2 self.client = ClientV2(self.directory, self.net) self.new_reg = self.new_reg.update(terms_of_service_agreed=True) @@ -752,11 +102,40 @@ class ClientV2Test(ClientTestBase): self.assertEqual(self.regr, self.client.new_account(self.new_reg)) + def test_new_account_tos_link(self): + self.response.status_code = http_client.CREATED + self.response.json.return_value = self.regr.body.to_json() + self.response.headers['Location'] = self.regr.uri + self.response.links.update({ + 'terms-of-service': {'url': 'https://www.letsencrypt-demo.org/tos'}, + }) + + self.assertEqual(self.client.new_account(self.new_reg).terms_of_service, + 'https://www.letsencrypt-demo.org/tos') + + def test_new_account_conflict(self): self.response.status_code = http_client.OK self.response.headers['Location'] = self.regr.uri self.assertRaises(errors.ConflictError, self.client.new_account, self.new_reg) + def test_deactivate_account(self): + deactivated_regr = self.regr.update( + body=self.regr.body.update(status='deactivated')) + self.response.json.return_value = deactivated_regr.body.to_json() + self.response.status_code = http_client.OK + self.response.headers['Location'] = self.regr.uri + self.assertEqual(self.client.deactivate_registration(self.regr), deactivated_regr) + + def test_deactivate_authorization(self): + deactivated_authz = self.authzr.update( + body=self.authzr.body.update(status=messages.STATUS_DEACTIVATED)) + self.response.json.return_value = deactivated_authz.body.to_json() + authzr = self.client.deactivate_authorization(self.authzr) + self.assertEqual(deactivated_authz.body, authzr.body) + self.assertEqual(self.client.net.post.call_count, 1) + self.assertIn(self.authzr.uri, self.net.post.call_args_list[0][0]) + def test_new_order(self): order_response = copy.deepcopy(self.response) order_response.status_code = http_client.CREATED @@ -775,6 +154,20 @@ class ClientV2Test(ClientTestBase): mock_post_as_get.side_effect = (authz_response, authz_response2) self.assertEqual(self.client.new_order(CSR_MIXED_PEM), self.orderr) + def test_answer_challege(self): + self.response.links['up'] = {'url': self.challr.authzr_uri} + self.response.json.return_value = self.challr.body.to_json() + chall_response = challenges.DNSResponse(validation=None) + self.client.answer_challenge(self.challr.body, chall_response) + + self.assertRaises(errors.UnexpectedUpdate, self.client.answer_challenge, + self.challr.body.update(uri='foo'), chall_response) + + def test_answer_challenge_missing_next(self): + self.assertRaises( + errors.ClientError, self.client.answer_challenge, + self.challr.body, challenges.DNSResponse(validation=None)) + @mock.patch('acme.client.datetime') def test_poll_and_finalize(self, mock_datetime): mock_datetime.datetime.now.return_value = datetime.datetime(2018, 2, 15) @@ -821,6 +214,11 @@ class ClientV2Test(ClientTestBase): self.authz.to_json(), self.authz2.to_json(), updated_authz2.to_json()) self.assertEqual(self.client.poll_authorizations(self.orderr, deadline), updated_orderr) + def test_poll_unexpected_update(self): + updated_authz = self.authz.update(identifier=self.identifier.update(value='foo')) + self.response.json.return_value = updated_authz.to_json() + self.assertRaises(errors.UnexpectedUpdate, self.client.poll, self.authzr) + def test_finalize_order_success(self): updated_order = self.order.update( certificate='https://www.letsencrypt-demo.org/acme/cert/', @@ -872,9 +270,9 @@ class ClientV2Test(ClientTestBase): deadline = datetime.datetime(9999, 9, 9) resp = self.client.finalize_order(self.orderr, deadline, fetch_alternative_chains=True) self.net.post.assert_any_call('https://example.com/acme/cert/1', - mock.ANY, acme_version=2, new_nonce_url=mock.ANY) + mock.ANY, new_nonce_url=mock.ANY) self.net.post.assert_any_call('https://example.com/acme/cert/2', - mock.ANY, acme_version=2, new_nonce_url=mock.ANY) + mock.ANY, new_nonce_url=mock.ANY) self.assertEqual(resp, updated_orderr) del self.response.headers['Link'] @@ -884,8 +282,15 @@ class ClientV2Test(ClientTestBase): def test_revoke(self): self.client.revoke(messages_test.CERT, self.rsn) self.net.post.assert_called_once_with( - self.directory["revokeCert"], mock.ANY, acme_version=2, - new_nonce_url=DIRECTORY_V2['newNonce']) + self.directory["revokeCert"], mock.ANY, new_nonce_url=DIRECTORY_V2['newNonce']) + + def test_revoke_bad_status_raises_error(self): + self.response.status_code = http_client.METHOD_NOT_ALLOWED + self.assertRaises( + errors.ClientError, + self.client.revoke, + messages_test.CERT, + self.rsn) def test_update_registration(self): # "Instance of 'Field' has no to_json/update member" bug: @@ -916,6 +321,11 @@ class ClientV2Test(ClientTestBase): def test_external_account_required_default(self): self.assertFalse(self.client.external_account_required()) + def test_query_registration_client(self): + self.response.json.return_value = self.regr.body.to_json() + self.response.headers['Location'] = 'https://www.letsencrypt-demo.org/acme/reg/1' + self.assertEqual(self.regr, self.client.query_registration(self.regr)) + def test_post_as_get(self): with mock.patch('acme.client.ClientV2._authzr_from_response') as mock_client: mock_client.return_value = self.authzr2 @@ -923,12 +333,64 @@ class ClientV2Test(ClientTestBase): self.client.poll(self.authzr2) # pylint: disable=protected-access self.client.net.post.assert_called_once_with( - self.authzr2.uri, None, acme_version=2, + self.authzr2.uri, None, new_nonce_url='https://www.letsencrypt-demo.org/acme/new-nonce') self.client.net.get.assert_not_called() + def test_retry_after_date(self): + self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' + self.assertEqual( + datetime.datetime(1999, 12, 31, 23, 59, 59), + self.client.retry_after(response=self.response, default=10)) -class MockJSONDeSerializable(VersionedLEACMEMixin, jose.JSONDeSerializable): + @mock.patch('acme.client.datetime') + def test_retry_after_invalid(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.response.headers['Retry-After'] = 'foooo' + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 10), + self.client.retry_after(response=self.response, default=10)) + + @mock.patch('acme.client.datetime') + def test_retry_after_overflow(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + dt_mock.datetime.side_effect = datetime.datetime + + self.response.headers['Retry-After'] = "Tue, 116 Feb 2016 11:50:00 MST" + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 10), + self.client.retry_after(response=self.response, default=10)) + + @mock.patch('acme.client.datetime') + def test_retry_after_seconds(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.response.headers['Retry-After'] = '50' + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 50), + self.client.retry_after(response=self.response, default=10)) + + @mock.patch('acme.client.datetime') + def test_retry_after_missing(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 10), + self.client.retry_after(response=self.response, default=10)) + + def test_get_directory(self): + self.response.json.return_value = DIRECTORY_V2.to_json() + self.assertEqual( + DIRECTORY_V2.to_partial_json(), + ClientV2.get_directory('https://example.com/dir', self.net).to_partial_json()) + + +class MockJSONDeSerializable(jose.JSONDeSerializable): # pylint: disable=missing-docstring def __init__(self, value): self.value = value @@ -963,8 +425,7 @@ class ClientNetworkTest(unittest.TestCase): def test_wrap_in_jws(self): # pylint: disable=protected-access jws_dump = self.net._wrap_in_jws( - MockJSONDeSerializable('foo'), nonce=b'Tg', url="url", - acme_version=1) + MockJSONDeSerializable('foo'), nonce=b'Tg', url="url") jws = acme_jws.JWS.json_loads(jws_dump) self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'}) self.assertEqual(jws.signature.combined.nonce, b'Tg') @@ -973,8 +434,7 @@ class ClientNetworkTest(unittest.TestCase): self.net.account = {'uri': 'acct-uri'} # pylint: disable=protected-access jws_dump = self.net._wrap_in_jws( - MockJSONDeSerializable('foo'), nonce=b'Tg', url="url", - acme_version=2) + MockJSONDeSerializable('foo'), nonce=b'Tg', url="url") jws = acme_jws.JWS.json_loads(jws_dump) self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'}) self.assertEqual(jws.signature.combined.nonce, b'Tg') @@ -1080,14 +540,13 @@ class ClientNetworkTest(unittest.TestCase): self.net.session = mock.MagicMock() self.net.session.request.return_value = mock.MagicMock( ok=True, status_code=http_client.OK, - headers={"Content-Type": "application/pkix-cert"}, content=b"hi") # pylint: disable=protected-access self.net._send_request('HEAD', 'http://example.com/', 'foo', - timeout=mock.ANY, bar='baz') + timeout=mock.ANY, bar='baz', headers={'Accept': 'application/pkix-cert'}) mock_logger.debug.assert_called_with( 'Received response:\nHTTP %d\n%s\n\n%s', 200, - 'Content-Type: application/pkix-cert', b'aGk=') + '', b'aGk=') def test_send_request_post(self): self.net.session = mock.MagicMock() @@ -1260,13 +719,13 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): 'uri', self.obj, content_type=self.content_type)) self.assertTrue(self.response.checked) self.net._wrap_in_jws.assert_called_once_with( - self.obj, jose.b64decode(self.all_nonces.pop()), "uri", 1) + self.obj, jose.b64decode(self.all_nonces.pop()), "uri") self.available_nonces = [] self.assertRaises(errors.MissingNonce, self.net.post, 'uri', self.obj, content_type=self.content_type) self.net._wrap_in_jws.assert_called_with( - self.obj, jose.b64decode(self.all_nonces.pop()), "uri", 1) + self.obj, jose.b64decode(self.all_nonces.pop()), "uri") def test_post_wrong_initial_nonce(self): # HEAD self.available_nonces = [b'f', jose.b64encode(b'good')] @@ -1324,14 +783,13 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): check_response = mock.MagicMock() self.net._check_response = check_response self.assertRaises(errors.ClientError, self.net.post, 'uri', - self.obj, content_type=self.content_type, acme_version=2, + self.obj, content_type=self.content_type, new_nonce_url='new_nonce_uri') self.assertEqual(check_response.call_count, 1) def test_new_nonce_uri_removed(self): self.content_type = None - self.net.post('uri', self.obj, content_type=None, - acme_version=2, new_nonce_url='new_nonce_uri') + self.net.post('uri', self.obj, content_type=None, new_nonce_url='new_nonce_uri') class ClientNetworkSourceAddressBindingTest(unittest.TestCase): diff --git a/acme/tests/fields_test.py b/acme/tests/fields_test.py index 4cc167f9c..b53798f00 100644 --- a/acme/tests/fields_test.py +++ b/acme/tests/fields_test.py @@ -54,19 +54,5 @@ class RFC3339FieldTest(unittest.TestCase): jose.DeserializationError, RFC3339Field.default_decoder, '') -class ResourceTest(unittest.TestCase): - """Tests for acme.fields.Resource.""" - - def setUp(self): - from acme.fields import Resource - self.field = Resource('x') - - def test_decode_good(self): - self.assertEqual('x', self.field.decode('x')) - - def test_decode_wrong(self): - self.assertRaises(jose.DeserializationError, self.field.decode, 'y') - - if __name__ == '__main__': unittest.main() # pragma: no cover diff --git a/acme/tests/magic_typing_test.py b/acme/tests/magic_typing_test.py deleted file mode 100644 index d470337bd..000000000 --- a/acme/tests/magic_typing_test.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Tests for acme.magic_typing.""" -import sys -import unittest -import warnings -from unittest import mock - - -class MagicTypingTest(unittest.TestCase): - """Tests for acme.magic_typing.""" - def test_import_success(self): - try: - import typing as temp_typing - except ImportError: # pragma: no cover - temp_typing = None # pragma: no cover - typing_class_mock = mock.MagicMock() - text_mock = mock.MagicMock() - typing_class_mock.Text = text_mock - sys.modules['typing'] = typing_class_mock - if 'acme.magic_typing' in sys.modules: - del sys.modules['acme.magic_typing'] # pragma: no cover - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - from acme.magic_typing import Text - self.assertEqual(Text, text_mock) - del sys.modules['acme.magic_typing'] - sys.modules['typing'] = temp_typing - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/tests/messages_test.py b/acme/tests/messages_test.py index cf7e7629a..cce4e409f 100644 --- a/acme/tests/messages_test.py +++ b/acme/tests/messages_test.py @@ -134,8 +134,8 @@ class DirectoryTest(unittest.TestCase): def setUp(self): from acme.messages import Directory self.dir = Directory({ - 'new-reg': 'reg', - mock.MagicMock(resource_type='new-cert'): 'cert', + 'newReg': 'reg', + 'newCert': 'cert', 'meta': Directory.Meta( terms_of_service='https://example.com/acme/terms', website='https://www.example.com/', @@ -148,26 +148,23 @@ class DirectoryTest(unittest.TestCase): Directory({'foo': 'bar'}) def test_getitem(self): - self.assertEqual('reg', self.dir['new-reg']) - from acme.messages import NewRegistration - self.assertEqual('reg', self.dir[NewRegistration]) - self.assertEqual('reg', self.dir[NewRegistration()]) + self.assertEqual('reg', self.dir['newReg']) def test_getitem_fails_with_key_error(self): self.assertRaises(KeyError, self.dir.__getitem__, 'foo') def test_getattr(self): - self.assertEqual('reg', self.dir.new_reg) + self.assertEqual('reg', self.dir.newReg) def test_getattr_fails_with_attribute_error(self): self.assertRaises(AttributeError, self.dir.__getattr__, 'foo') def test_to_json(self): self.assertEqual(self.dir.to_json(), { - 'new-reg': 'reg', - 'new-cert': 'cert', + 'newReg': 'reg', + 'newCert': 'cert', 'meta': { - 'terms-of-service': 'https://example.com/acme/terms', + 'termsOfService': 'https://example.com/acme/terms', 'website': 'https://www.example.com/', 'caaIdentities': ['example.com'], }, @@ -287,7 +284,7 @@ class UpdateRegistrationTest(unittest.TestCase): def test_empty(self): from acme.messages import UpdateRegistration jstring = '{"resource": "reg"}' - self.assertEqual(jstring, UpdateRegistration().json_dumps()) + self.assertEqual('{}', UpdateRegistration().json_dumps()) self.assertEqual( UpdateRegistration(), UpdateRegistration.json_loads(jstring)) @@ -335,7 +332,7 @@ class ChallengeBodyTest(unittest.TestCase): error=error) self.jobj_to = { - 'uri': 'http://challb', + 'url': 'http://challb', 'status': self.status, 'type': 'dns', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', @@ -382,20 +379,17 @@ class AuthorizationTest(unittest.TestCase): chall=challenges.DNS( token=b'DGyRejmCefe7v4NfDGDKfA')), ) - combinations = ((0,), (1,)) from acme.messages import Authorization from acme.messages import Identifier from acme.messages import IDENTIFIER_FQDN identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com') self.authz = Authorization( - identifier=identifier, combinations=combinations, - challenges=self.challbs) + identifier=identifier, challenges=self.challbs) self.jobj_from = { 'identifier': identifier.to_json(), 'challenges': [challb.to_json() for challb in self.challbs], - 'combinations': combinations, } def test_from_json(self): @@ -406,12 +400,6 @@ class AuthorizationTest(unittest.TestCase): from acme.messages import Authorization hash(Authorization.from_json(self.jobj_from)) - def test_resolved_combinations(self): - self.assertEqual(self.authz.resolved_combinations, ( - (self.challbs[0],), - (self.challbs[1],), - )) - class AuthorizationResourceTest(unittest.TestCase): """Tests for acme.messages.AuthorizationResource.""" @@ -502,7 +490,6 @@ class JWSPayloadRFC8555Compliant(unittest.TestCase): from acme.messages import NewAuthorization new_order = NewAuthorization() - new_order.le_acme_version = 2 jobj = new_order.json_dumps(indent=2).encode() # RFC8555 states that JWS bodies must not have a resource field. diff --git a/certbot/certbot/_internal/account.py b/certbot/certbot/_internal/account.py index d8f002948..8ebc71de3 100644 --- a/certbot/certbot/_internal/account.py +++ b/certbot/certbot/_internal/account.py @@ -20,7 +20,7 @@ import pytz from acme import fields as acme_fields from acme import messages -from acme.client import ClientBase +from acme.client import ClientV2 from certbot import configuration from certbot import errors from certbot import interfaces @@ -114,7 +114,7 @@ class AccountMemoryStorage(interfaces.AccountStorage): def find_all(self) -> List[Account]: return list(self.accounts.values()) - def save(self, account: Account, client: ClientBase) -> None: + def save(self, account: Account, client: ClientV2) -> None: if account.id in self.accounts: logger.debug("Overwriting account: %s", account.id) self.accounts[account.id] = account @@ -243,11 +243,11 @@ class AccountFileStorage(interfaces.AccountStorage): def load(self, account_id: str) -> Account: return self._load_for_server_path(account_id, self.config.server_path) - def save(self, account: Account, client: ClientBase) -> None: + def save(self, account: Account, client: ClientV2) -> None: """Create a new account. :param Account account: account to create - :param ClientBase client: ACME client associated to the account + :param ClientV2 client: ACME client associated to the account """ try: @@ -258,11 +258,11 @@ class AccountFileStorage(interfaces.AccountStorage): except IOError as error: raise errors.AccountStorageError(error) - def update_regr(self, account: Account, client: ClientBase) -> None: + def update_regr(self, account: Account, client: ClientV2) -> None: """Update the registration resource. :param Account account: account to update - :param ClientBase client: ACME client associated to the account + :param ClientV2 client: ACME client associated to the account """ try: @@ -358,7 +358,7 @@ class AccountFileStorage(interfaces.AccountStorage): with util.safe_open(self._key_path(dir_path), "w", chmod=0o400) as key_file: key_file.write(account.key.json_dumps()) - def _update_regr(self, account: Account, acme: ClientBase, dir_path: str) -> None: + def _update_regr(self, account: Account, acme: ClientV2, dir_path: str) -> None: with open(self._regr_path(dir_path), "w") as regr_file: regr = account.regr # If we have a value for new-authz, save it for forwards diff --git a/certbot/certbot/_internal/auth_handler.py b/certbot/certbot/_internal/auth_handler.py index 979ef0220..05feaadc0 100644 --- a/certbot/certbot/_internal/auth_handler.py +++ b/certbot/certbot/_internal/auth_handler.py @@ -36,7 +36,7 @@ class AuthHandler: :class:`~acme.challenges.Challenge` types :type auth: certbot.interfaces.Authenticator - :ivar acme.client.BackwardsCompatibleClientV2 acme_client: ACME client API. + :ivar acme.client.ClientV2 acme_client: ACME client API. :ivar account: Client's Account :type account: :class:`certbot._internal.account.Account` @@ -226,15 +226,10 @@ class AuthHandler: logger.info("Performing the following challenges:") for authzr in pending_authzrs: authzr_challenges = authzr.body.challenges - if self.acme.acme_version == 1: - combinations = authzr.body.combinations - else: - combinations = tuple((i,) for i in range(len(authzr_challenges))) path = gen_challenge_path( authzr_challenges, - self._get_chall_pref(authzr.body.identifier.value), - combinations) + self._get_chall_pref(authzr.body.identifier.value)) achalls.extend(self._challenge_factory(authzr, path)) @@ -387,12 +382,9 @@ def challb_to_achall(challb: messages.ChallengeBody, account_key: josepy.JWK, def gen_challenge_path(challbs: List[messages.ChallengeBody], - preferences: List[Type[challenges.Challenge]], - combinations: Tuple[Tuple[int, ...], ...]) -> Tuple[int, ...]: + preferences: List[Type[challenges.Challenge]]) -> Tuple[int, ...]: """Generate a plan to get authority over the identity. - .. todo:: This can be possibly be rewritten to use resolved_combinations. - :param tuple challbs: A tuple of challenges (:class:`acme.messages.Challenge`) from :class:`acme.messages.AuthorizationResource` to be @@ -402,10 +394,6 @@ def gen_challenge_path(challbs: List[messages.ChallengeBody], :param list preferences: List of challenge preferences for domain (:class:`acme.challenges.Challenge` subclasses) - :param tuple combinations: A collection of sets of challenges from - :class:`acme.messages.Challenge`, each of which would - be sufficient to prove possession of the identifier. - :returns: list of indices from ``challenges``. :rtype: list @@ -413,21 +401,6 @@ def gen_challenge_path(challbs: List[messages.ChallengeBody], path cannot be created that satisfies the CA given the preferences and combinations. - """ - if combinations: - return _find_smart_path(challbs, preferences, combinations) - return _find_dumb_path(challbs, preferences) - - -def _find_smart_path(challbs: List[messages.ChallengeBody], - preferences: List[Type[challenges.Challenge]], - combinations: Tuple[Tuple[int, ...], ...] - ) -> Tuple[int, ...]: - """Find challenge path with server hints. - - Can be called if combinations is included. Function uses a simple - ranking system to choose the combo with the lowest cost. - """ chall_cost = {} max_cost = 1 @@ -441,6 +414,8 @@ def _find_smart_path(challbs: List[messages.ChallengeBody], # Set above completing all of the available challenges best_combo_cost = max_cost + combinations = tuple((i,) for i in range(len(challbs))) + combo_total = 0 for combo in combinations: for challenge_index in combo: @@ -459,28 +434,6 @@ def _find_smart_path(challbs: List[messages.ChallengeBody], return best_combo -def _find_dumb_path(challbs: List[messages.ChallengeBody], - preferences: List[Type[challenges.Challenge]]) -> Tuple[int, ...]: - """Find challenge path without server hints. - - Should be called if the combinations hint is not included by the - server. This function either returns a path containing all - challenges provided by the CA or raises an exception. - - """ - path = [] - for i, challb in enumerate(challbs): - # supported is set to True if the challenge type is supported - supported = next((True for pref_c in preferences - if isinstance(challb.chall, pref_c)), False) - if supported: - path.append(i) - else: - raise _report_no_chall_path(challbs) - - return tuple(path) - - def _report_no_chall_path(challbs: List[messages.ChallengeBody]) -> errors.AuthorizationError: """Logs and return a raisable error reporting that no satisfiable chall path exists. diff --git a/certbot/certbot/_internal/client.py b/certbot/certbot/_internal/client.py index bcd9713db..89c0e498a 100644 --- a/certbot/certbot/_internal/client.py +++ b/certbot/certbot/_internal/client.py @@ -10,7 +10,6 @@ from typing import IO from typing import List from typing import Optional from typing import Tuple -import warnings from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric.rsa import generate_private_key @@ -70,16 +69,8 @@ def acme_from_config_key(config: configuration.NamespaceConfig, key: jose.JWK, verify_ssl=(not config.no_verify_ssl), user_agent=determine_user_agent(config)) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - - client = acme_client.BackwardsCompatibleClientV2(net, key, config.server) - if client.acme_version == 1: - logger.warning( - "Certbot is configured to use an ACMEv1 server (%s). ACMEv1 support is deprecated" - " and will soon be removed. See https://community.letsencrypt.org/t/143839 for " - "more information.", config.server) - return cast(acme_client.ClientV2, client) + directory = acme_client.ClientV2.get_directory(config.server, net) + return acme_client.ClientV2(directory, net) def determine_user_agent(config: configuration.NamespaceConfig) -> str: @@ -256,18 +247,13 @@ def perform_registration(acme: acme_client.ClientV2, config: configuration.Names " Please use --eab-kid and --eab-hmac-key.") raise errors.Error(msg) + tos = acme.directory.meta.terms_of_service + if tos_cb and tos: + tos_cb(tos) + try: - newreg = messages.NewRegistration.from_data( - email=config.email, external_account_binding=eab) - # Until ACME v1 support is removed from Certbot, we actually need the provided - # ACME client to be a wrapper of type BackwardsCompatibleClientV2. - # TODO: Remove this cast and rewrite the logic when the client is actually a ClientV2 - try: - return cast(acme_client.BackwardsCompatibleClientV2, - acme).new_account_and_tos(newreg, tos_cb) - except AttributeError: - raise errors.Error("The ACME client must be an instance of " - "acme.client.BackwardsCompatibleClientV2") + return acme.new_account(messages.NewRegistration.from_data( + email=config.email, terms_of_service_agreed=True, external_account_binding=eab)) except messages.Error as e: if e.code in ("invalidEmail", "invalidContact"): if config.noninteractive_mode: @@ -291,8 +277,8 @@ class Client: :ivar .Authenticator auth: Prepared (`.Authenticator.prepare`) authenticator that can solve ACME challenges. :ivar .Installer installer: Installer. - :ivar acme.client.BackwardsCompatibleClientV2 acme: Optional ACME - client API handle. You might already have one from `register`. + :ivar acme.client.ClientV2 acme: Optional ACME client API handle. You might + already have one from `register`. """ diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py index 0d12ffd2e..00c37303a 100644 --- a/certbot/certbot/interfaces.py +++ b/certbot/certbot/interfaces.py @@ -18,7 +18,7 @@ import zope.interface from acme.challenges import Challenge from acme.challenges import ChallengeResponse -from acme.client import ClientBase +from acme.client import ClientV2 from certbot import configuration from certbot.achallenges import AnnotatedChallenge @@ -53,7 +53,7 @@ class AccountStorage(metaclass=ABCMeta): raise NotImplementedError() @abstractmethod - def save(self, account: 'Account', client: ClientBase) -> None: # pragma: no cover + def save(self, account: 'Account', client: ClientV2) -> None: # pragma: no cover """Save account. :raises .AccountStorageError: if account could not be saved diff --git a/certbot/certbot/tests/acme_util.py b/certbot/certbot/tests/acme_util.py index d8ee7f9a8..6412f5dc7 100644 --- a/certbot/certbot/tests/acme_util.py +++ b/certbot/certbot/tests/acme_util.py @@ -3,7 +3,6 @@ import datetime from typing import Any from typing import Dict from typing import Iterable -from typing import Tuple import josepy as jose @@ -24,12 +23,6 @@ DNS01_2 = challenges.DNS01(token=b"cafecafecafecafecafecafe0feedbac") CHALLENGES = [HTTP01, DNS01] -def gen_combos(challbs: Iterable[messages.ChallengeBody]) -> Tuple[Tuple[int], ...]: - """Generate natural combinations for challbs.""" - # completing a single DV challenge satisfies the CA - return tuple((i,) for i, _ in enumerate(challbs)) - - def chall_to_challb(chall: challenges.Challenge, status: messages.Status) -> messages.ChallengeBody: """Return ChallengeBody from Challenge.""" kwargs = { @@ -61,15 +54,13 @@ ACHALLENGES = [HTTP01_A, DNS01_A] def gen_authzr(authz_status: messages.Status, domain: str, challs: Iterable[challenges.Challenge], - statuses: Iterable[messages.Status], - combos: bool = True) -> messages.AuthorizationResource: + statuses: Iterable[messages.Status]) -> messages.AuthorizationResource: """Generate an authorization resource. :param authz_status: Status object :type authz_status: :class:`acme.messages.Status` :param list challs: Challenge objects :param list statuses: status of each challenge object - :param bool combos: Whether or not to add combinations """ challbs = tuple( @@ -81,8 +72,6 @@ def gen_authzr(authz_status: messages.Status, domain: str, challs: Iterable[chal typ=messages.IDENTIFIER_FQDN, value=domain), "challenges": challbs, } - if combos: - authz_kwargs.update({"combinations": gen_combos(challbs)}) if authz_status == messages.STATUS_VALID: authz_kwargs.update({ "status": authz_status, diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index e13dfbfe5..9adbcdfbc 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -33,7 +33,7 @@ class ChallengeFactoryTest(unittest.TestCase): self.authzr = acme_util.gen_authzr( messages.STATUS_PENDING, "test", acme_util.CHALLENGES, - [messages.STATUS_PENDING] * 6, False) + [messages.STATUS_PENDING] * 6) def test_all(self): achalls = self.handler._challenge_factory( @@ -81,7 +81,6 @@ class HandleAuthorizationsTest(unittest.TestCase): self.mock_account = mock.MagicMock() self.mock_net = mock.MagicMock(spec=acme_client.ClientV2) - self.mock_net.acme_version = 1 self.mock_net.retry_after.side_effect = acme_client.ClientV2.retry_after self.handler = AuthHandler( @@ -92,8 +91,8 @@ class HandleAuthorizationsTest(unittest.TestCase): def tearDown(self): logging.disable(logging.NOTSET) - def _test_name1_http_01_1_common(self, combos): - authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=combos) + def _test_name1_http_01_1_common(self): + authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES) mock_order = mock.MagicMock(authorizations=[authzr]) self.mock_net.poll.side_effect = _gen_mock_on_poll(retry=1, wait_value=30) @@ -117,39 +116,14 @@ class HandleAuthorizationsTest(unittest.TestCase): self.assertEqual(len(authzr), 1) - def test_name1_http_01_1_acme_1(self): - self._test_name1_http_01_1_common(combos=True) - def test_name1_http_01_1_acme_2(self): - self.mock_net.acme_version = 2 - self._test_name1_http_01_1_common(combos=False) - - def test_name1_http_01_1_dns_1_acme_1(self): - self.mock_net.poll.side_effect = _gen_mock_on_poll() - self.mock_auth.get_chall_pref.return_value.append(challenges.DNS01) - - authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=False) - mock_order = mock.MagicMock(authorizations=[authzr]) - authzr = self.handler.handle_authorizations(mock_order, self.mock_config) - - self.assertEqual(self.mock_net.answer_challenge.call_count, 2) - - self.assertEqual(self.mock_net.poll.call_count, 1) - - self.assertEqual(self.mock_auth.cleanup.call_count, 1) - # Test if list first element is http-01, use typ because it is an achall - for achall in self.mock_auth.cleanup.call_args[0][0]: - self.assertIn(achall.typ, ["http-01", "dns-01"]) - - # Length of authorizations list - self.assertEqual(len(authzr), 1) + self._test_name1_http_01_1_common() def test_name1_http_01_1_dns_1_acme_2(self): - self.mock_net.acme_version = 2 self.mock_net.poll.side_effect = _gen_mock_on_poll() self.mock_auth.get_chall_pref.return_value.append(challenges.DNS01) - authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=False) + authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES) mock_order = mock.MagicMock(authorizations=[authzr]) authzr = self.handler.handle_authorizations(mock_order, self.mock_config) @@ -165,7 +139,7 @@ class HandleAuthorizationsTest(unittest.TestCase): # Length of authorizations list self.assertEqual(len(authzr), 1) - def _test_name3_http_01_3_common(self, combos): + def test_name3_http_01_3_common_acme_2(self): authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES), gen_dom_authzr(domain="1", challs=acme_util.CHALLENGES), gen_dom_authzr(domain="2", challs=acme_util.CHALLENGES)] @@ -183,13 +157,6 @@ class HandleAuthorizationsTest(unittest.TestCase): self.assertEqual(len(authzr), 3) - def test_name3_http_01_3_common_acme_1(self): - self._test_name3_http_01_3_common(combos=True) - - def test_name3_http_01_3_common_acme_2(self): - self.mock_net.acme_version = 2 - self._test_name3_http_01_3_common(combos=False) - def test_debug_challenges(self): config = mock.Mock(debug_challenges=True, verbose_count=0) authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] @@ -269,8 +236,8 @@ class HandleAuthorizationsTest(unittest.TestCase): self.assertRaises(errors.AuthorizationError, self.handler.handle_authorizations, mock_order, self.mock_config) - def _test_preferred_challenge_choice_common(self, combos): - authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=combos)] + def test_preferred_challenge_choice_common_acme_2(self): + authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] mock_order = mock.MagicMock(authorizations=authzrs) self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01) @@ -285,28 +252,14 @@ class HandleAuthorizationsTest(unittest.TestCase): self.assertEqual( self.mock_auth.cleanup.call_args[0][0][0].typ, "http-01") - def test_preferred_challenge_choice_common_acme_1(self): - self._test_preferred_challenge_choice_common(combos=True) - - def test_preferred_challenge_choice_common_acme_2(self): - self.mock_net.acme_version = 2 - self._test_preferred_challenge_choice_common(combos=False) - - def _test_preferred_challenges_not_supported_common(self, combos): - authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=combos)] + def test_preferred_challenges_not_supported_acme_2(self): + authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] mock_order = mock.MagicMock(authorizations=authzrs) self.handler.pref_challs.append(challenges.DNS01.typ) self.assertRaises( errors.AuthorizationError, self.handler.handle_authorizations, mock_order, self.mock_config) - def test_preferred_challenges_not_supported_acme_1(self): - self._test_preferred_challenges_not_supported_common(combos=True) - - def test_preferred_challenges_not_supported_acme_2(self): - self.mock_net.acme_version = 2 - self._test_preferred_challenges_not_supported_common(combos=False) - def test_dns_only_challenge_not_supported(self): authzrs = [gen_dom_authzr(domain="0", challs=[acme_util.DNS01])] mock_order = mock.MagicMock(authorizations=authzrs) @@ -317,7 +270,7 @@ class HandleAuthorizationsTest(unittest.TestCase): def test_perform_error(self): self.mock_auth.perform.side_effect = errors.AuthorizationError - authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=True) + authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES) mock_order = mock.MagicMock(authorizations=[authzr]) self.assertRaises(errors.AuthorizationError, self.handler.handle_authorizations, mock_order, self.mock_config) @@ -392,7 +345,7 @@ class HandleAuthorizationsTest(unittest.TestCase): authzr = acme_util.gen_authzr( messages.STATUS_PENDING, "0", [acme_util.DNS01], - [messages.STATUS_PENDING], False) + [messages.STATUS_PENDING]) mock_order = mock.MagicMock(authorizations=[authzr]) self.assertRaises( errors.AuthorizationError, self.handler.handle_authorizations, @@ -404,7 +357,7 @@ class HandleAuthorizationsTest(unittest.TestCase): authzr = acme_util.gen_authzr( messages.STATUS_VALID, "0", [acme_util.DNS01], - [messages.STATUS_VALID], False) + [messages.STATUS_VALID]) mock_order = mock.MagicMock(authorizations=[authzr]) self.handler.handle_authorizations(mock_order, self.mock_config) @@ -426,7 +379,7 @@ class HandleAuthorizationsTest(unittest.TestCase): ("is_valid_but_will_fail", messages.STATUS_VALID)] to_deactivate = [acme_util.gen_authzr(a[1], a[0], [acme_util.HTTP01], - [a[1], False]) for a in to_deactivate] + [a[1]]) for a in to_deactivate] orderr = mock.MagicMock(authorizations=to_deactivate) self.mock_net.deactivate_authorization.side_effect = _mock_deactivate @@ -452,8 +405,7 @@ def _gen_mock_on_poll(status=messages.STATUS_VALID, retry=0, wait_value=1): effective_status, authzr.body.identifier.value, [challb.chall for challb in authzr.body.challenges], - [effective_status] * len(authzr.body.challenges), - authzr.body.combinations) + [effective_status] * len(authzr.body.challenges)) return updated_azr, mock.MagicMock(headers={'Retry-After': str(wait_value)}) return _mock @@ -477,8 +429,6 @@ class ChallbToAchallTest(unittest.TestCase): class GenChallengePathTest(unittest.TestCase): """Tests for certbot._internal.auth_handler.gen_challenge_path. - .. todo:: Add more tests for dumb_path... depending on what we want to do. - """ def setUp(self): logging.disable(logging.FATAL) @@ -487,34 +437,25 @@ class GenChallengePathTest(unittest.TestCase): logging.disable(logging.NOTSET) @classmethod - def _call(cls, challbs, preferences, combinations): + def _call(cls, challbs, preferences): from certbot._internal.auth_handler import gen_challenge_path - return gen_challenge_path(challbs, preferences, combinations) + return gen_challenge_path(challbs, preferences) def test_common_case(self): """Given DNS01 and HTTP01 with appropriate combos.""" challbs = (acme_util.DNS01_P, acme_util.HTTP01_P) prefs = [challenges.DNS01, challenges.HTTP01] - combos = ((0,), (1,)) - # Smart then trivial dumb path test - self.assertEqual(self._call(challbs, prefs, combos), (0,)) - self.assertTrue(self._call(challbs, prefs, None)) - # Rearrange order... - self.assertEqual(self._call(challbs[::-1], prefs, combos), (1,)) - self.assertTrue(self._call(challbs[::-1], prefs, None)) + self.assertEqual(self._call(challbs, prefs), (0,)) + self.assertEqual(self._call(challbs[::-1], prefs), (1,)) def test_not_supported(self): - challbs = (acme_util.DNS01_P, acme_util.HTTP01_P) + challbs = (acme_util.DNS01_P,) prefs = [challenges.HTTP01] - combos = ((0, 1),) - # smart path fails because no challs in perfs satisfies combos + # smart path fails because no challs in prefs satisfies combos self.assertRaises( - errors.AuthorizationError, self._call, challbs, prefs, combos) - # dumb path fails because all challbs are not supported - self.assertRaises( - errors.AuthorizationError, self._call, challbs, prefs, None) + errors.AuthorizationError, self._call, challbs, prefs) class ReportFailedAuthzrsTest(unittest.TestCase): @@ -615,11 +556,11 @@ def gen_auth_resp(chall_list): for chall in chall_list] -def gen_dom_authzr(domain, challs, combos=True): +def gen_dom_authzr(domain, challs): """Generates new authzr for domains.""" return acme_util.gen_authzr( messages.STATUS_PENDING, domain, challs, - [messages.STATUS_PENDING] * len(challs), combos) + [messages.STATUS_PENDING] * len(challs)) if __name__ == "__main__": diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 28daefea8..e51be7f9b 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -69,13 +69,13 @@ class RegisterTest(test_util.ConfigTestCase): self.config.register_unsafely_without_email = False self.config.email = "alias@example.com" self.account_storage = account.AccountMemoryStorage() + self.tos_cb = mock.MagicMock() with mock.patch("zope.component.provideUtility"): display_obj.set_display(MagicMock()) def _call(self): from certbot._internal.client import register - tos_cb = mock.MagicMock() - return register(self.config, self.account_storage, tos_cb) + return register(self.config, self.account_storage, self.tos_cb) @staticmethod def _public_key_mock(): @@ -98,31 +98,42 @@ class RegisterTest(test_util.ConfigTestCase): @staticmethod @contextlib.contextmanager def _patched_acme_client(): - # This function is written this way to avoid deprecation warnings that - # are raised when BackwardsCompatibleClientV2 is accessed on the real - # acme.client module. with mock.patch('certbot._internal.client.acme_client') as mock_acme_client: - yield mock_acme_client.BackwardsCompatibleClientV2 + yield mock_acme_client.ClientV2 def test_no_tos(self): with self._patched_acme_client() as mock_client: - mock_client.new_account_and_tos().terms_of_service = "http://tos" + mock_client.new_account().terms_of_service = "http://tos" mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.prepare_subscription") as mock_prepare: - mock_client().new_account_and_tos.side_effect = errors.Error + mock_client().new_account.side_effect = errors.Error self.assertRaises(errors.Error, self._call) self.assertIs(mock_prepare.called, False) - mock_client().new_account_and_tos.side_effect = None + mock_client().new_account.side_effect = None self._call() self.assertIs(mock_prepare.called, True) + @mock.patch('certbot._internal.eff.prepare_subscription') + def test_empty_meta(self, unused_mock_prepare): + # Test that we can handle an ACME server which does not implement the 'meta' + # directory object (for terms-of-service handling). + with self._patched_acme_client() as mock_client: + from acme.messages import Directory + mock_client().directory = Directory.from_json({}) + + mock_client().external_account_required.side_effect = self._false_mock + + self._call() + self.assertIs(self.tos_cb.called, False) + @test_util.patch_display_util() def test_it(self, unused_mock_get_utility): with self._patched_acme_client() as mock_client: mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.handle_subscription"): self._call() + self.assertIs(self.tos_cb.called, True) @mock.patch("certbot._internal.client.display_ops.get_email") def test_email_retry(self, mock_get_email): @@ -133,7 +144,7 @@ class RegisterTest(test_util.ConfigTestCase): with self._patched_acme_client() as mock_client: mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.prepare_subscription") as mock_prepare: - mock_client().new_account_and_tos.side_effect = [mx_err, mock.MagicMock()] + mock_client().new_account.side_effect = [mx_err, mock.MagicMock()] self._call() self.assertEqual(mock_get_email.call_count, 1) self.assertIs(mock_prepare.called, True) @@ -146,7 +157,7 @@ class RegisterTest(test_util.ConfigTestCase): with self._patched_acme_client() as mock_client: mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.handle_subscription"): - mock_client().new_account_and_tos.side_effect = [mx_err, mock.MagicMock()] + mock_client().new_account.side_effect = [mx_err, mock.MagicMock()] self.assertRaises(errors.Error, self._call) def test_needs_email(self): @@ -176,7 +187,7 @@ class RegisterTest(test_util.ConfigTestCase): # check Certbot did not ask the user to provide an email self.assertIs(mock_get_email.called, False) # 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) + self.assertFalse(mock_client().new_account.call_args[0][0].contact) @test_util.patch_display_util() def test_with_eab_arguments(self, unused_mock_get_utility): @@ -228,7 +239,7 @@ class RegisterTest(test_util.ConfigTestCase): ) mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.handle_subscription") as mock_handle: - mock_client().new_account_and_tos.side_effect = [mx_err, mock.MagicMock()] + mock_client().new_account.side_effect = [mx_err, mock.MagicMock()] self.assertRaises(messages.Error, self._call) self.assertIs(mock_handle.called, False) @@ -245,7 +256,7 @@ class ClientTestCommon(test_util.ConfigTestCase): from certbot._internal.client import Client with mock.patch("certbot._internal.client.acme_client") as acme: - self.acme_client = acme.BackwardsCompatibleClientV2 + self.acme_client = acme.ClientV2 self.acme = self.acme_client.return_value = mock.MagicMock() self.client_network = acme.ClientNetwork self.client = Client( diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 61632fc8f..ad443f1ca 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -388,7 +388,7 @@ class RevokeTest(test_util.TempDirTestCase): mock.patch('certbot._internal.main._determine_account'), mock.patch('certbot._internal.main.display_ops.success_revocation') ] - self.mock_acme_client = patches[0].start().BackwardsCompatibleClientV2 + self.mock_acme_client = patches[0].start().ClientV2 patches[1].start() self.mock_determine_account = patches[2].start() self.mock_success_revoke = patches[3].start() @@ -418,12 +418,19 @@ class RevokeTest(test_util.TempDirTestCase): from certbot._internal.main import revoke revoke(config, plugins) + def _mock_set_by_cli(self, mocked: mock.MagicMock, key: str, value: bool) -> None: + def set_by_cli(k: str) -> bool: + if key == k: + return value + return mock.DEFAULT + mocked.side_effect = set_by_cli + @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.main.client.acme_client') def test_revoke_with_reason(self, mock_acme_client, mock_delete_if_appropriate): mock_delete_if_appropriate.return_value = False - mock_revoke = mock_acme_client.BackwardsCompatibleClientV2().revoke + mock_revoke = mock_acme_client.ClientV2().revoke expected = [] for reason, code in constants.REVOCATION_REASONS.items(): args = 'revoke --cert-path={0} --reason {1}'.format(self.tmp_cert_path, reason).split() @@ -438,42 +445,56 @@ class RevokeTest(test_util.TempDirTestCase): @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.storage.RenewableCert') @mock.patch('certbot._internal.storage.renewal_file_for_certname') - def test_revoke_by_certname(self, unused_mock_renewal_file_for_certname, - mock_cert, mock_delete_if_appropriate): + @mock.patch('certbot._internal.client.acme_from_config_key') + @mock.patch('certbot._internal.cli.set_by_cli') + def test_revoke_by_certname(self, mock_set_by_cli, mock_acme_from_config, + unused_mock_renewal_file_for_certname, mock_cert, + mock_delete_if_appropriate): + self._mock_set_by_cli(mock_set_by_cli, "server", False) + mock_acme_from_config.return_value = self.mock_acme_client mock_cert.return_value = mock.MagicMock(cert_path=self.tmp_cert_path, server="https://acme.example") args = 'revoke --cert-name=example.com'.split() mock_delete_if_appropriate.return_value = False self._call(args) - self.mock_acme_client.assert_called_once_with(mock.ANY, mock.ANY, 'https://acme.example') + self.assertEqual(mock_acme_from_config.call_args_list[0][0][0].server, + 'https://acme.example') self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path) @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.storage.RenewableCert') @mock.patch('certbot._internal.storage.renewal_file_for_certname') - def test_revoke_by_certname_and_server(self, unused_mock_renewal_file_for_certname, - mock_cert, mock_delete_if_appropriate): + @mock.patch('certbot._internal.client.acme_from_config_key') + @mock.patch('certbot._internal.cli.set_by_cli') + def test_revoke_by_certname_and_server(self, mock_set_by_cli, mock_acme_from_config, + unused_mock_renewal_file_for_certname, mock_cert, + mock_delete_if_appropriate): """Revoking with --server should use the server from the CLI""" + self._mock_set_by_cli(mock_set_by_cli, "server", True) mock_cert.return_value = mock.MagicMock(cert_path=self.tmp_cert_path, server="https://acme.example") args = 'revoke --cert-name=example.com --server https://other.example'.split() mock_delete_if_appropriate.return_value = False self._call(args) - self.mock_acme_client.assert_called_once_with(mock.ANY, mock.ANY, 'https://other.example') + self.assertEqual(mock_acme_from_config.call_args_list[0][0][0].server, + 'https://other.example') self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path) @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.storage.RenewableCert') @mock.patch('certbot._internal.storage.renewal_file_for_certname') - def test_revoke_by_certname_empty_server(self, unused_mock_renewal_file_for_certname, + @mock.patch('certbot._internal.client.acme_from_config_key') + @mock.patch('certbot._internal.cli.set_by_cli') + def test_revoke_by_certname_empty_server(self, mock_set_by_cli, mock_acme_from_config, + unused_mock_renewal_file_for_certname, mock_cert, mock_delete_if_appropriate): """Revoking with --cert-name where the lineage server is empty shouldn't crash """ mock_cert.return_value = mock.MagicMock(cert_path=self.tmp_cert_path, server=None) args = 'revoke --cert-name=example.com'.split() mock_delete_if_appropriate.return_value = False self._call(args) - self.mock_acme_client.assert_called_once_with( - mock.ANY, mock.ANY, constants.CLI_DEFAULTS['server']) + self.assertEqual(mock_acme_from_config.call_args_list[0][0][0].server, + constants.CLI_DEFAULTS['server']) self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path) @mock.patch('certbot._internal.main._delete_if_appropriate') @@ -1566,11 +1587,12 @@ class MainTest(test_util.ConfigTestCase): self._call_no_clientmock(['--cert-path', SS_CERT_PATH, '--key-path', RSA2048_KEY_PATH, '--server', server, 'revoke']) with open(RSA2048_KEY_PATH, 'rb') as f: - mock_acme_client.BackwardsCompatibleClientV2.assert_called_once_with( - mock.ANY, jose.JWK.load(f.read()), server) + self.assertEqual(mock_acme_client.ClientV2.call_count, 1) + self.assertEqual(mock_acme_client.ClientNetwork.call_args[0][0], + jose.JWK.load(f.read())) with open(SS_CERT_PATH, 'rb') as f: cert = crypto_util.pyopenssl_load_certificate(f.read())[0] - mock_revoke = mock_acme_client.BackwardsCompatibleClientV2().revoke + mock_revoke = mock_acme_client.ClientV2().revoke mock_revoke.assert_called_once_with( jose.ComparableX509(cert), mock.ANY) From 20ca9288d5c55f47a9dbbafe3b9fe4157308cb77 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Tue, 6 Sep 2022 19:55:58 -0700 Subject: [PATCH 18/29] Add UI text recommending multi-domain certs (#9393) * Suggest multi-domain certs in domain selection menu * Update changelog * lint: fix long line Co-authored-by: Alex Zorin --- certbot/CHANGELOG.md | 1 + certbot/certbot/display/ops.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 22c2ffdba..06a415777 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -23,6 +23,7 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). next major release of Certbot. * The `source_address` argument for `acme.client.ClientNetwork` is deprecated and support for it will be removed in the next major release. +* Add UI text suggesting users create certs for multiple domains, when possible ### Fixed diff --git a/certbot/certbot/display/ops.py b/certbot/certbot/display/ops.py index f07093b55..d5b2c2420 100644 --- a/certbot/certbot/display/ops.py +++ b/certbot/certbot/display/ops.py @@ -181,7 +181,10 @@ def _filter_names(names: Iterable[str], if override_question: question = override_question else: - question = "Which names would you like to activate HTTPS for?" + question = ( + "Which names would you like to activate HTTPS for?\n" + "We recommend selecting either all domains, or all domains in a VirtualHost/server " + "block.") code, names = display_util.checklist( question, tags=sorted_names, cli_flag="--domains", force_interactive=True) return code, [str(s) for s in names] From 5e247d1683721cfc50b2c0d3de395155e82790f9 Mon Sep 17 00:00:00 2001 From: alexzorin Date: Wed, 7 Sep 2022 13:00:05 +1000 Subject: [PATCH 19/29] unexport attributes in certbot.display.util (#9358) --- .../certbot_compatibility_test/test_driver.py | 3 +- certbot/certbot/_internal/main.py | 14 ++--- certbot/certbot/display/util.py | 56 ------------------- certbot/tests/display/obj_test.py | 2 +- 4 files changed, 9 insertions(+), 66 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 26b7660ab..6a29e74f5 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -33,7 +33,6 @@ from acme import messages from certbot import achallenges from certbot import errors as le_errors from certbot._internal.display import obj as display_obj -from certbot.display import util as display_util from certbot.tests import acme_util DESCRIPTION = """ @@ -339,7 +338,7 @@ def setup_logging(args: argparse.Namespace) -> None: def setup_display() -> None: """"Prepares a display utility instance for the Certbot plugins """ - displayer = display_util.NoninteractiveDisplay(sys.stdout) + displayer = display_obj.NoninteractiveDisplay(sys.stdout) display_obj.set_display(displayer) diff --git a/certbot/certbot/_internal/main.py b/certbot/certbot/_internal/main.py index 098ce3243..f6d3e02e1 100644 --- a/certbot/certbot/_internal/main.py +++ b/certbot/certbot/_internal/main.py @@ -1654,8 +1654,8 @@ def make_or_verify_needed_dirs(config: configuration.NamespaceConfig) -> None: @contextmanager def make_displayer(config: configuration.NamespaceConfig - ) -> Generator[Union[display_util.NoninteractiveDisplay, - display_util.FileDisplay], None, None]: + ) -> Generator[Union[display_obj.NoninteractiveDisplay, + display_obj.FileDisplay], None, None]: """Creates a display object appropriate to the flags in the supplied config. :param config: Configuration object @@ -1663,18 +1663,18 @@ def make_displayer(config: configuration.NamespaceConfig :returns: Display object """ - displayer: Union[None, display_util.NoninteractiveDisplay, - display_util.FileDisplay] = None + displayer: Union[None, display_obj.NoninteractiveDisplay, + display_obj.FileDisplay] = None devnull: Optional[IO] = None if config.quiet: config.noninteractive_mode = True devnull = open(os.devnull, "w") # pylint: disable=consider-using-with - displayer = display_util.NoninteractiveDisplay(devnull) + displayer = display_obj.NoninteractiveDisplay(devnull) elif config.noninteractive_mode: - displayer = display_util.NoninteractiveDisplay(sys.stdout) + displayer = display_obj.NoninteractiveDisplay(sys.stdout) else: - displayer = display_util.FileDisplay( + displayer = display_obj.FileDisplay( sys.stdout, config.force_interactive) try: diff --git a/certbot/certbot/display/util.py b/certbot/certbot/display/util.py index 06c64b56e..defa0a9db 100644 --- a/certbot/certbot/display/util.py +++ b/certbot/certbot/display/util.py @@ -9,26 +9,12 @@ should be used whenever: Other messages can use the `logging` module. See `log.py`. """ -import sys -from types import ModuleType -from typing import Any -from typing import cast from typing import List from typing import Optional from typing import Tuple from typing import Union -import warnings from certbot._internal.display import obj -# These specific imports from certbot._internal.display.obj and -# certbot._internal.display.util are done to not break the public API of this -# module. -from certbot._internal.display.obj import FileDisplay # pylint: disable=unused-import -from certbot._internal.display.obj import NoninteractiveDisplay # pylint: disable=unused-import -from certbot._internal.display.obj import SIDE_FRAME # pylint: disable=unused-import -from certbot._internal.display.util import input_with_timeout # pylint: disable=unused-import -from certbot._internal.display.util import separate_list_input # pylint: disable=unused-import -from certbot._internal.display.util import summarize_domain_list # pylint: disable=unused-import # These constants are defined this way to make them easier to document with # Sphinx and to not couple our public docstrings to our internal ones. @@ -38,17 +24,8 @@ OK = obj.OK CANCEL = obj.CANCEL """Display exit code for a user canceling the display.""" -# These constants are unused and should be removed in a major release of -# Certbot. WIDTH = 72 -HELP = "help" -"""Display exit code when for when the user requests more help. (UNUSED)""" - -ESC = "esc" -"""Display exit code when the user hits Escape (UNUSED)""" - - def notify(msg: str) -> None: """Display a basic status message. @@ -204,36 +181,3 @@ def assert_valid_call(prompt: str, default: str, cli_flag: str, force_interactiv msg += ("\nYou can set an answer to " "this prompt with the {0} flag".format(cli_flag)) assert default is not None or force_interactive, msg - - -# This class takes a similar approach to the cryptography project to deprecate attributes -# in public modules. See the _ModuleWithDeprecation class here: -# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 -class _DisplayUtilDeprecationModule: - """ - Internal class delegating to a module, and displaying warnings when attributes - related to deprecated attributes in the certbot.display.util module. - """ - def __init__(self, module: ModuleType) -> None: - self.__dict__['_module'] = module - - def __getattr__(self, attr: str) -> Any: - if attr in ('FileDisplay', 'NoninteractiveDisplay', 'SIDE_FRAME', 'input_with_timeout', - 'separate_list_input', 'summarize_domain_list', 'WIDTH', 'HELP', 'ESC'): - warnings.warn('{0} attribute in certbot.display.util module is deprecated ' - 'and will be removed soon.'.format(attr), - DeprecationWarning, stacklevel=2) - return getattr(self._module, attr) - - def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover - setattr(self._module, attr, value) - - def __delattr__(self, attr: str) -> None: # pragma: no cover - delattr(self._module, attr) - - def __dir__(self) -> List[str]: # pragma: no cover - return ['_module'] + dir(self._module) - - -# Patching ourselves to warn about deprecation and planned removal of some elements in the module. -sys.modules[__name__] = cast(ModuleType, _DisplayUtilDeprecationModule(sys.modules[__name__])) diff --git a/certbot/tests/display/obj_test.py b/certbot/tests/display/obj_test.py index f6fe41a68..4da2c3b3b 100644 --- a/certbot/tests/display/obj_test.py +++ b/certbot/tests/display/obj_test.py @@ -275,7 +275,7 @@ class NoninteractiveDisplayTest(unittest.TestCase): """Test non-interactive display. These tests are pretty easy!""" def setUp(self): self.mock_stdout = mock.MagicMock() - self.displayer = display_util.NoninteractiveDisplay(self.mock_stdout) + self.displayer = display_obj.NoninteractiveDisplay(self.mock_stdout) @mock.patch("certbot._internal.display.obj.logger") def test_notification_no_pause(self, mock_logger): From a4a23155378f8766d0b0ff1b1471ebfb1bc13cbe Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Tue, 6 Sep 2022 20:20:56 -0700 Subject: [PATCH 20/29] Removed deprecated functions (#9314) * Removed deprecated functions * rm import of distutils.version * revert changelog Co-authored-by: Alex Zorin --- certbot/certbot/tests/util.py | 63 +++++++---------------------------- certbot/certbot/util.py | 22 ------------ certbot/tests/util_test.py | 13 -------- 3 files changed, 12 insertions(+), 86 deletions(-) diff --git a/certbot/certbot/tests/util.py b/certbot/certbot/tests/util.py index 532220f4d..dbff31a14 100644 --- a/certbot/certbot/tests/util.py +++ b/certbot/certbot/tests/util.py @@ -198,56 +198,18 @@ def make_lineage(config_dir: str, testfile: str, ec: bool = False) -> str: return conf_path -def patch_get_utility(target: str = 'zope.component.getUtility') -> mock.MagicMock: - """Deprecated, patch certbot.display.util directly or use patch_display_util instead. - - :param str target: path to patch - - :returns: mock zope.component.getUtility - :rtype: mock.MagicMock - - """ - warnings.warn('Decorator certbot.tests.util.patch_get_utility is deprecated. You should now ' - 'patch certbot.display.util yourself directly or use ' - 'certbot.tests.util.patch_display_util as a temporary workaround.') - return cast(mock.MagicMock, mock.patch(target, new_callable=_create_display_util_mock)) - - -def patch_get_utility_with_stdout(target: str = 'zope.component.getUtility', - stdout: Optional[IO] = None) -> mock.MagicMock: - """Deprecated, patch certbot.display.util directly - or use patch_display_util_with_stdout instead. - - :param str target: path to patch - :param object stdout: object to write standard output to; it is - expected to have a `write` method - - :returns: mock zope.component.getUtility - :rtype: mock.MagicMock - - """ - warnings.warn('Decorator certbot.tests.util.patch_get_utility_with_stdout is deprecated. You ' - 'should now patch certbot.display.util yourself directly or use ' - 'use certbot.tests.util.patch_display_util_with_stdout as a temporary ' - 'workaround.') - stdout = stdout if stdout else io.StringIO() - freezable_mock = _create_display_util_mock_with_stdout(stdout) - return cast(mock.MagicMock, mock.patch(target, new=freezable_mock)) - - def patch_display_util() -> mock.MagicMock: """Patch certbot.display.util to use a special mock display utility. The mock display utility works like a regular mock object, except it also also asserts that methods are called with valid arguments. - The mock created by this patch mocks out Certbot internals so this can be - used like the old patch_get_utility function. That is, the mock object will - be called by the certbot.display.util functions and the mock returned by - that call will be used as the display utility. This was done to simplify - the transition from zope.component and mocking certbot.display.util - functions directly in test code should be preferred over using this - function in the future. + The mock created by this patch mocks out Certbot internals. That is, the + mock object will be called by the certbot.display.util functions and the + mock returned by that call will be used as the display utility. This was + done to simplify the transition from zope.component and mocking + certbot.display.util functions directly in test code should be preferred + over using this function in the future. See https://github.com/certbot/certbot/issues/8948 @@ -267,13 +229,12 @@ def patch_display_util_with_stdout( The mock display utility works like a regular mock object, except it also asserts that methods are called with valid arguments. - The mock created by this patch mocks out Certbot internals so this can be - used like the old patch_get_utility function. That is, the mock object will - be called by the certbot.display.util functions and the mock returned by - that call will be used as the display utility. This was done to simplify - the transition from zope.component and mocking certbot.display.util - functions directly in test code should be preferred over using this - function in the future. + The mock created by this patch mocks out Certbot internals. That is, the + mock object will be called by the certbot.display.util functions and the + mock returned by that call will be used as the display utility. This was + done to simplify the transition from zope.component and mocking + certbot.display.util functions directly in test code should be preferred + over using this function in the future. See https://github.com/certbot/certbot/issues/8948 diff --git a/certbot/certbot/util.py b/certbot/certbot/util.py index 242dfef5a..12507ef36 100644 --- a/certbot/certbot/util.py +++ b/certbot/certbot/util.py @@ -17,7 +17,6 @@ from typing import List from typing import Optional from typing import Set from typing import Tuple -from typing import TYPE_CHECKING from typing import Union import warnings @@ -33,9 +32,6 @@ _USE_DISTRO = sys.platform.startswith('linux') if _USE_DISTRO: import distro -if TYPE_CHECKING: - import distutils.version - logger = logging.getLogger(__name__) @@ -611,24 +607,6 @@ def is_wildcard_domain(domain: Union[str, bytes]) -> bool: return domain.startswith(b"*.") -def get_strict_version(normalized: str) -> "distutils.version.StrictVersion": - """Converts a normalized version to a strict version. - - :param str normalized: normalized version string - - :returns: An equivalent strict version - :rtype: distutils.version.StrictVersion - - """ - warnings.warn("certbot.util.get_strict_version is deprecated and will be " - "removed in a future release.", DeprecationWarning) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - import distutils.version - # strict version ending with "a" and a number designates a pre-release - return distutils.version.StrictVersion(normalized.replace(".dev", "a")) - - def is_staging(srv: str) -> bool: """ Determine whether a given ACME server is a known test / staging server. diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index 3af87f85a..0da0976b8 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -592,19 +592,6 @@ class OsInfoTest(unittest.TestCase): self.assertEqual(cbutil.get_python_os_info(), ("testdist", "42")) -class GetStrictVersionTest(unittest.TestCase): - """Test for certbot.util.get_strict_version.""" - - @classmethod - def _call(cls, *args, **kwargs): - from certbot.util import get_strict_version - return get_strict_version(*args, **kwargs) - - def test_it(self): - with self.assertWarnsRegex(DeprecationWarning, "get_strict_version"): - self._call("1.2.3") - - class AtexitRegisterTest(unittest.TestCase): """Tests for certbot.util.atexit_register.""" def setUp(self): From 529a0e2272b3b5cc52f2b51314541632d5322940 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Tue, 6 Sep 2022 20:31:21 -0700 Subject: [PATCH 21/29] Remove deprecated functions (#9315) * Remove deprecated functions * rm unused imports * actually remove execute_command! * revert changelog Co-authored-by: Alex Zorin --- certbot/certbot/compat/misc.py | 38 +----------------- certbot/certbot/crypto_util.py | 64 ------------------------------- certbot/tests/compat/misc_test.py | 53 ++++--------------------- certbot/tests/crypto_util_test.py | 35 ----------------- 4 files changed, 9 insertions(+), 181 deletions(-) diff --git a/certbot/certbot/compat/misc.py b/certbot/certbot/compat/misc.py index 8ca876962..d40f3fa65 100644 --- a/certbot/certbot/compat/misc.py +++ b/certbot/certbot/compat/misc.py @@ -10,7 +10,6 @@ import subprocess import sys from typing import Optional from typing import Tuple -import warnings from certbot import errors from certbot.compat import os @@ -144,8 +143,8 @@ def execute_command_status(cmd_name: str, shell_cmd: str, subprocess.run(shell=True) - on Windows command will be run in a Powershell shell - This differs from execute_command: it returns the exit code, and does not log the result - and output of the command. + This function returns the exit code, and does not log the result and output + of the command. :param str cmd_name: the user facing name of the hook being run :param str shell_cmd: shell command to execute @@ -168,36 +167,3 @@ def execute_command_status(cmd_name: str, shell_cmd: str, # bytes in Python 3 out, err = proc.stdout, proc.stderr return proc.returncode, err, out - - -def execute_command(cmd_name: str, shell_cmd: str, env: Optional[dict] = None) -> Tuple[str, str]: - """ - Run a command: - - on Linux command will be run by the standard shell selected with - subprocess.run(shell=True) - - on Windows command will be run in a Powershell shell - - This differs from execute_command: it returns the exit code, and does not log the result - and output of the command. - - :param str cmd_name: the user facing name of the hook being run - :param str shell_cmd: shell command to execute - :param dict env: environ to pass into subprocess.run - - :returns: `tuple` (`str` stderr, `str` stdout) - """ - # Deprecation per https://github.com/certbot/certbot/issues/8854 - warnings.warn( - "execute_command will be deprecated in the future, use execute_command_status instead", - PendingDeprecationWarning - ) - returncode, err, out = execute_command_status(cmd_name, shell_cmd, env) - base_cmd = os.path.basename(shell_cmd.split(None, 1)[0]) - if out: - logger.info('Output from %s command %s:\n%s', cmd_name, base_cmd, out) - if returncode != 0: - logger.error('%s command "%s" returned error code %d', - cmd_name, shell_cmd, returncode) - if err: - logger.error('Error output from %s command %s:\n%s', cmd_name, base_cmd, err) - return err, out diff --git a/certbot/certbot/crypto_util.py b/certbot/certbot/crypto_util.py index f45bf3505..a9a8269fe 100644 --- a/certbot/certbot/crypto_util.py +++ b/certbot/certbot/crypto_util.py @@ -15,7 +15,6 @@ from typing import Set from typing import Tuple from typing import TYPE_CHECKING from typing import Union -import warnings from cryptography import x509 from cryptography.exceptions import InvalidSignature @@ -35,7 +34,6 @@ import josepy from OpenSSL import crypto from OpenSSL import SSL import pyrfc3339 -import zope.component from acme import crypto_util as acme_crypto_util from certbot import errors @@ -100,41 +98,6 @@ def generate_key(key_size: int, key_dir: str, key_type: str = "rsa", return util.Key(key_path, key_pem) -# TODO: Remove this call once zope dependencies are removed from Certbot. -def init_save_key(key_size: int, key_dir: str, key_type: str = "rsa", - elliptic_curve: str = "secp256r1", - keyname: str = "key-certbot.pem") -> util.Key: - """Initializes and saves a privkey. - - Inits key and saves it in PEM format on the filesystem. - - .. note:: keyname is the attempted filename, it may be different if a file - already exists at the path. - - .. deprecated:: 1.16.0 - Use :func:`generate_key` instead. - - :param int key_size: key size in bits if key size is rsa. - :param str key_dir: Key save directory. - :param str key_type: Key Type [rsa, ecdsa] - :param str elliptic_curve: Name of the elliptic curve if key type is ecdsa. - :param str keyname: Filename of key - - :returns: Key - :rtype: :class:`certbot.util.Key` - - :raises ValueError: If unable to generate the key given key_size. - - """ - warnings.warn("certbot.crypto_util.init_save_key is deprecated, please use " - "certbot.crypto_util.generate_key instead.", DeprecationWarning) - - config = zope.component.getUtility(interfaces.IConfig) - - return generate_key(key_size, key_dir, key_type=key_type, elliptic_curve=elliptic_curve, - keyname=keyname, strict_permissions=config.strict_permissions) - - def generate_csr(privkey: util.Key, names: Union[List[str], Set[str]], path: str, must_staple: bool = False, strict_permissions: bool = True) -> util.CSR: """Initialize a CSR with the given private key. @@ -165,33 +128,6 @@ def generate_csr(privkey: util.Key, names: Union[List[str], Set[str]], path: str return util.CSR(csr_filename, csr_pem, "pem") -# TODO: Remove this call once zope dependencies are removed from Certbot. -def init_save_csr(privkey: util.Key, names: Set[str], path: str) -> util.CSR: - """Initialize a CSR with the given private key. - - .. deprecated:: 1.16.0 - Use :func:`generate_csr` instead. - - :param privkey: Key to include in the CSR - :type privkey: :class:`certbot.util.Key` - - :param set names: `str` names to include in the CSR - - :param str path: Certificate save directory. - - :returns: CSR - :rtype: :class:`certbot.util.CSR` - - """ - warnings.warn("certbot.crypto_util.init_save_csr is deprecated, please use " - "certbot.crypto_util.generate_csr instead.", DeprecationWarning) - - config = zope.component.getUtility(interfaces.IConfig) - - return generate_csr(privkey, names, path, must_staple=config.must_staple, - strict_permissions=config.strict_permissions) - - # WARNING: the csr and private key file are possible attack vectors for TOCTOU # We should either... # A. Do more checks to verify that the CSR is trusted/valid diff --git a/certbot/tests/compat/misc_test.py b/certbot/tests/compat/misc_test.py index f64d8891f..2155bd5a0 100644 --- a/certbot/tests/compat/misc_test.py +++ b/certbot/tests/compat/misc_test.py @@ -9,52 +9,7 @@ import warnings from certbot.compat import os - -class ExecuteTest(unittest.TestCase): - """Tests for certbot.compat.misc.execute_command.""" - - @classmethod - def _call(cls, *args, **kwargs): - from certbot.compat.misc import execute_command - # execute_command is superseded by execute_command_status - with warnings.catch_warnings(): - warnings.simplefilter('ignore', category=PendingDeprecationWarning) - return execute_command(*args, **kwargs) - - def test_it(self): - for returncode in range(0, 2): - for stdout in ("", "Hello World!",): - for stderr in ("", "Goodbye Cruel World!"): - self._test_common(returncode, stdout, stderr) - - def _test_common(self, returncode, stdout, stderr): - given_command = "foo" - given_name = "foo-hook" - with mock.patch("certbot.compat.misc.subprocess.run") as mock_run: - mock_run.return_value.stdout = stdout - mock_run.return_value.stderr = stderr - mock_run.return_value.returncode = returncode - with mock.patch("certbot.compat.misc.logger") as mock_logger: - self.assertEqual(self._call(given_name, given_command), (stderr, stdout)) - - executed_command = mock_run.call_args[1].get( - "args", mock_run.call_args[0][0]) - if os.name == 'nt': - expected_command = ['powershell.exe', '-Command', given_command] - else: - expected_command = given_command - self.assertEqual(executed_command, expected_command) - - mock_logger.info.assert_any_call("Running %s command: %s", - given_name, given_command) - if stdout: - mock_logger.info.assert_any_call(mock.ANY, mock.ANY, - mock.ANY, stdout) - if stderr or returncode: - self.assertIs(mock_logger.error.called, True) - - -class ExecuteStatusTest(ExecuteTest): +class ExecuteStatusTest(unittest.TestCase): """Tests for certbot.compat.misc.execute_command_status.""" @classmethod @@ -84,6 +39,12 @@ class ExecuteStatusTest(ExecuteTest): mock_logger.info.assert_any_call("Running %s command: %s", given_name, given_command) + def test_it(self): + for returncode in range(0, 2): + for stdout in ("", "Hello World!",): + for stderr in ("", "Goodbye Cruel World!"): + self._test_common(returncode, stdout, stderr) + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index 858db079c..0263ff76b 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -67,23 +67,6 @@ class GenerateKeyTest(test_util.TempDirTestCase): self.assertRaises(ValueError, self._call, 431, self.workdir) -class InitSaveKey(unittest.TestCase): - """Test for certbot.crypto_util.init_save_key.""" - @mock.patch("certbot.crypto_util.generate_key") - @mock.patch("certbot.crypto_util.zope.component") - def test_it(self, mock_zope, mock_generate): - from certbot.crypto_util import init_save_key - - mock_zope.getUtility.return_value = mock.MagicMock(strict_permissions=True) - - with self.assertWarns(DeprecationWarning): - init_save_key(4096, "/some/path") - - mock_generate.assert_called_with(4096, "/some/path", elliptic_curve="secp256r1", - key_type="rsa", keyname="key-certbot.pem", - strict_permissions=True) - - class GenerateCSRTest(test_util.TempDirTestCase): """Tests for certbot.crypto_util.generate_csr.""" @mock.patch('acme.crypto_util.make_csr') @@ -100,24 +83,6 @@ class GenerateCSRTest(test_util.TempDirTestCase): self.assertIn('csr-certbot.pem', csr.file) -class InitSaveCsr(unittest.TestCase): - """Tests for certbot.crypto_util.init_save_csr.""" - @mock.patch("certbot.crypto_util.generate_csr") - @mock.patch("certbot.crypto_util.zope.component") - def test_it(self, mock_zope, mock_generate): - from certbot.crypto_util import init_save_csr - - mock_zope.getUtility.return_value = mock.MagicMock(must_staple=True, - strict_permissions=True) - key = certbot.util.Key(file=None, pem=None) - - with self.assertWarns(DeprecationWarning): - init_save_csr(key, {"dummy"}, "/some/path") - - mock_generate.assert_called_with(key, {"dummy"}, "/some/path", - must_staple=True, strict_permissions=True) - - class ValidCSRTest(unittest.TestCase): """Tests for certbot.crypto_util.valid_csr.""" From 9d736d5c9c939ef5a47b001ef9cb3ea97122c1c4 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Wed, 7 Sep 2022 07:09:32 +0200 Subject: [PATCH 22/29] Remove zope from Certbot (#9161) * Remove zope and the internal reporter util. * remove zope references from .pylintrc and pytest.ini Co-authored-by: Alex Zorin --- .pylintrc | 9 +- certbot/certbot/_internal/display/obj.py | 15 +-- certbot/certbot/_internal/main.py | 22 +--- certbot/certbot/_internal/plugins/disco.py | 106 +----------------- .../certbot/_internal/plugins/selection.py | 6 +- certbot/certbot/_internal/renewal.py | 5 - certbot/certbot/_internal/reporter.py | 94 ---------------- certbot/certbot/interfaces.py | 69 +----------- certbot/setup.py | 2 - certbot/tests/auth_handler_test.py | 3 +- certbot/tests/cli_test.py | 2 +- certbot/tests/client_test.py | 3 +- certbot/tests/crypto_util_test.py | 2 - certbot/tests/main_test.py | 12 +- certbot/tests/plugins/disco_test.py | 32 ------ certbot/tests/plugins/selection_test.py | 8 +- certbot/tests/reporter_test.py | 89 --------------- pytest.ini | 15 +-- tools/oldest_constraints.txt | 46 ++++---- tools/pinning/oldest/pyproject.toml | 5 - tools/requirements.txt | 6 +- 21 files changed, 49 insertions(+), 502 deletions(-) delete mode 100644 certbot/certbot/_internal/reporter.py delete mode 100644 certbot/tests/reporter_test.py diff --git a/.pylintrc b/.pylintrc index d2730018d..89495c507 100644 --- a/.pylintrc +++ b/.pylintrc @@ -283,10 +283,6 @@ ignored-modules=pkg_resources,confargparse,argparse # (useful for classes with attributes dynamically set). ignored-classes=Field,Header,JWS,closing -# When zope mode is activated, add a predefined set of Zope acquired attributes -# to generated-members. -zope=yes - # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. Python regular # expressions are accepted. @@ -313,9 +309,8 @@ int-import-graph= [CLASSES] -# List of interface methods to ignore, separated by a comma. This is used for -# instance to not check methods defined in Zope's Interface base class. -ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by,implementedBy,providedBy +# List of interface methods to ignore, separated by a comma. +ignore-iface-methods= # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp diff --git a/certbot/certbot/_internal/display/obj.py b/certbot/certbot/_internal/display/obj.py index c5c2e388b..54c248b0c 100644 --- a/certbot/certbot/_internal/display/obj.py +++ b/certbot/certbot/_internal/display/obj.py @@ -10,11 +10,7 @@ from typing import Tuple from typing import TypeVar from typing import Union -import zope.component -import zope.interface - from certbot import errors -from certbot import interfaces from certbot._internal import constants from certbot._internal.display import completer from certbot._internal.display import util @@ -34,6 +30,7 @@ SIDE_FRAME = ("- " * 39) + "-" """Display boundary (alternates spaces, so when copy-pasted, markdown doesn't interpret it as a heading)""" + # This class holds the global state of the display service. Using this class # eliminates potential gotchas that exist if self.display was just a global # variable. In particular, in functions `_DISPLAY = ` would create a @@ -50,9 +47,6 @@ _SERVICE = _DisplayService() T = TypeVar("T") -# This use of IDisplay can be removed when this class is no longer accessible -# through the public API in certbot.display.util. -@zope.interface.implementer(interfaces.IDisplay) class FileDisplay: """File-based display.""" # see https://github.com/certbot/certbot/issues/3915 @@ -410,9 +404,6 @@ class FileDisplay: return OK, selection -# This use of IDisplay can be removed when this class is no longer accessible -# through the public API in certbot.display.util. -@zope.interface.implementer(interfaces.IDisplay) class NoninteractiveDisplay: """A display utility implementation that never asks for interactive user input""" @@ -573,8 +564,4 @@ def set_display(display: Union[FileDisplay, NoninteractiveDisplay]) -> None: :param Union[FileDisplay, NoninteractiveDisplay] display: the display service """ - # This call is done only for retro-compatibility purposes. - # TODO: Remove this call once zope dependencies are removed from Certbot. - zope.component.provideUtility(display, interfaces.IDisplay) - _SERVICE.display = display diff --git a/certbot/certbot/_internal/main.py b/certbot/certbot/_internal/main.py index f6d3e02e1..278a67473 100644 --- a/certbot/certbot/_internal/main.py +++ b/certbot/certbot/_internal/main.py @@ -17,8 +17,6 @@ from typing import Union import configobj import josepy as jose -import zope.component -import zope.interface from acme import client as acme_client from acme import errors as acme_errors @@ -38,7 +36,6 @@ from certbot._internal import eff from certbot._internal import hooks from certbot._internal import log from certbot._internal import renewal -from certbot._internal import reporter from certbot._internal import snap_config from certbot._internal import storage from certbot._internal import updater @@ -1165,15 +1162,14 @@ def plugins_cmd(config: configuration.NamespaceConfig, return filtered.init(config) - verified = filtered.verify(ifaces) - logger.debug("Verified plugins: %r", verified) + logger.debug("Filtered plugins: %r", filtered) if not config.prepare: - notify(str(verified)) + notify(str(filtered)) return - verified.prepare() - available = verified.available() + filtered.prepare() + available = filtered.available() logger.debug("Prepared plugins: %s", available) notify(str(available)) @@ -1716,10 +1712,6 @@ def main(cli_args: List[str] = None) -> Optional[Union[str, int]]: args = cli.prepare_and_parse_args(plugins, cli_args) config = configuration.NamespaceConfig(args) - # This call is done only for retro-compatibility purposes. - # TODO: Remove this call once zope dependencies are removed from Certbot. - zope.component.provideUtility(config, interfaces.IConfig) - # On windows, shell without administrative right cannot create symlinks required by certbot. # So we check the rights before continuing. misc.raise_for_non_administrative_windows_rights() @@ -1732,12 +1724,6 @@ def main(cli_args: List[str] = None) -> Optional[Union[str, int]]: if config.func != plugins_cmd: # pylint: disable=comparison-with-callable raise - # These calls are done only for retro-compatibility purposes. - # TODO: Remove these calls once zope dependencies are removed from Certbot. - report = reporter.Reporter(config) - zope.component.provideUtility(report, interfaces.IReporter) - util.atexit_register(report.print_messages) - with make_displayer(config) as displayer: display_obj.set_display(displayer) diff --git a/certbot/certbot/_internal/plugins/disco.py b/certbot/certbot/_internal/plugins/disco.py index 707f5ae9d..027e61838 100644 --- a/certbot/certbot/_internal/plugins/disco.py +++ b/certbot/certbot/_internal/plugins/disco.py @@ -12,11 +12,8 @@ from typing import Mapping from typing import Optional from typing import Type from typing import Union -import warnings import pkg_resources -import zope.interface -import zope.interface.verify from certbot import configuration from certbot import errors @@ -115,7 +112,7 @@ class PluginEntryPoint: def ifaces(self, *ifaces_groups: Iterable[Type]) -> bool: """Does plugin implements specified interface groups?""" return not ifaces_groups or any( - all(_implements(self.plugin_cls, iface) + all(issubclass(self.plugin_cls, iface) for iface in ifaces) for ifaces in ifaces_groups) @@ -133,16 +130,6 @@ class PluginEntryPoint: self._initialized = self.plugin_cls(config, self.name) return self._initialized - def verify(self, ifaces: Iterable[Type]) -> bool: - """Verify that the plugin conforms to the specified interfaces.""" - if not self.initialized: - raise ValueError("Plugin is not initialized.") - for iface in ifaces: # zope.interface.providedBy(plugin) - if not _verify(self.init(), self.plugin_cls, iface): - return False - - return True - @property def prepared(self) -> bool: """Has the plugin been prepared already?""" @@ -264,7 +251,7 @@ class PluginsRegistry(Mapping): plugin2 = other_ep.entry_point.dist.key if other_ep.entry_point.dist else "unknown" raise Exception("Duplicate plugin name {0} from {1} and {2}.".format( plugin_ep.name, plugin1, plugin2)) - if _provides(plugin_ep.plugin_cls, interfaces.Plugin): + if issubclass(plugin_ep.plugin_cls, interfaces.Plugin): plugins[plugin_ep.name] = plugin_ep else: # pragma: no cover logger.warning( @@ -299,10 +286,6 @@ class PluginsRegistry(Mapping): """Filter plugins based on interfaces.""" return self.filter(lambda p_ep: p_ep.ifaces(*ifaces_groups)) - def verify(self, ifaces: Iterable[Type]) -> "PluginsRegistry": - """Filter plugins based on verification.""" - return self.filter(lambda p_ep: p_ep.verify(ifaces)) - def prepare(self) -> List[Union[bool, Error]]: """Prepare all plugins in the registry.""" return [plugin_ep.prepare() for plugin_ep in self._plugins.values()] @@ -341,88 +324,3 @@ class PluginsRegistry(Mapping): if not self._plugins: return "No plugins" return "\n\n".join(str(p_ep) for p_ep in self._plugins.values()) - - -_DEPRECATION_PLUGIN = ("Zope interface certbot.interfaces.IPlugin is deprecated, " - "use ABC certbot.interface.Plugin instead.") - -_DEPRECATION_AUTHENTICATOR = ("Zope interface certbot.interfaces.IAuthenticator is deprecated, " - "use ABC certbot.interface.Authenticator instead.") - -_DEPRECATION_INSTALLER = ("Zope interface certbot.interfaces.IInstaller is deprecated, " - "use ABC certbot.interface.Installer instead.") - -_DEPRECATION_FACTORY = ("Zope interface certbot.interfaces.IPluginFactory is deprecated, " - "use ABC certbot.interface.Plugin instead.") - - -def _provides(target_class: Type[interfaces.Plugin], iface: Type) -> bool: - if issubclass(target_class, iface): - return True - - if iface == interfaces.Plugin and interfaces.IPluginFactory.providedBy(target_class): - logging.warning(_DEPRECATION_FACTORY) - warnings.warn(_DEPRECATION_FACTORY, DeprecationWarning) - return True - - return False - - -def _implements(target_class: Type[interfaces.Plugin], iface: Type) -> bool: - if issubclass(target_class, iface): - return True - - if iface == interfaces.Plugin and interfaces.IPlugin.implementedBy(target_class): - logging.warning(_DEPRECATION_PLUGIN) - warnings.warn(_DEPRECATION_PLUGIN, DeprecationWarning) - return True - - if iface == interfaces.Authenticator and interfaces.IAuthenticator.implementedBy(target_class): - logging.warning(_DEPRECATION_AUTHENTICATOR) - warnings.warn(_DEPRECATION_AUTHENTICATOR, DeprecationWarning) - return True - - if iface == interfaces.Installer and interfaces.IInstaller.implementedBy(target_class): - logging.warning(_DEPRECATION_INSTALLER) - warnings.warn(_DEPRECATION_INSTALLER, DeprecationWarning) - return True - - return False - - -def _verify(target_instance: interfaces.Plugin, target_class: Type[interfaces.Plugin], - iface: Type) -> bool: - if issubclass(target_class, iface): - # No need to trigger some verify logic for ABCs: when the object is instantiated, - # an error would be raised if implementation is not done properly. - # So the checks have been done effectively when the plugin has been initialized. - return True - - zope_iface: Optional[Type[zope.interface.Interface]] = None - message = "" - - if iface == interfaces.Plugin: - zope_iface = interfaces.IPlugin - message = _DEPRECATION_PLUGIN - if iface == interfaces.Authenticator: - zope_iface = interfaces.IAuthenticator - message = _DEPRECATION_AUTHENTICATOR - if iface == interfaces.Installer: - zope_iface = interfaces.IInstaller - message = _DEPRECATION_INSTALLER - - if not zope_iface: - raise ValueError(f"Unexpected type: {iface.__name__}") - - try: - zope.interface.verify.verifyObject(zope_iface, target_instance) - logging.warning(message) - warnings.warn(message, DeprecationWarning) - return True - except zope.interface.exceptions.BrokenImplementation as error: - if zope_iface.implementedBy(target_class): - logger.debug( - "%s implements %s but object does not verify: %s", - target_class, zope_iface.__name__, error, exc_info=True) - - return False diff --git a/certbot/certbot/_internal/plugins/selection.py b/certbot/certbot/_internal/plugins/selection.py index 826e1c932..708877d3e 100644 --- a/certbot/certbot/_internal/plugins/selection.py +++ b/certbot/certbot/_internal/plugins/selection.py @@ -65,7 +65,6 @@ def get_unprepared_installer(config: configuration.NamespaceConfig, return None installers = plugins.filter(lambda p_ep: p_ep.check_name(req_inst)) installers.init(config) - installers = installers.verify((interfaces.Installer,)) if len(installers) > 1: raise errors.PluginSelectionError( "Found multiple installers with the name %s, Certbot is unable to " @@ -116,9 +115,8 @@ def pick_plugin(config: configuration.NamespaceConfig, default: Optional[str], filtered = plugins.visible().ifaces(ifaces) filtered.init(config) - verified = filtered.verify(ifaces) - verified.prepare() - prepared = verified.available() + filtered.prepare() + prepared = filtered.available() if len(prepared) > 1: logger.debug("Multiple candidate plugins: %s", prepared) diff --git a/certbot/certbot/_internal/renewal.py b/certbot/certbot/_internal/renewal.py index 0ba2e8108..bd84901e1 100644 --- a/certbot/certbot/_internal/renewal.py +++ b/certbot/certbot/_internal/renewal.py @@ -19,12 +19,10 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import load_pem_private_key -import zope.component from certbot import configuration from certbot import crypto_util from certbot import errors -from certbot import interfaces from certbot import util from certbot._internal import cli from certbot._internal import client @@ -460,9 +458,6 @@ def handle_renewal_request(config: configuration.NamespaceConfig) -> None: if not renewal_candidate: parse_failures.append(renewal_file) else: - # This call is done only for retro-compatibility purposes. - # TODO: Remove this call once zope dependencies are removed from Certbot. - zope.component.provideUtility(lineage_config, interfaces.IConfig) renewal_candidate.ensure_deployed() from certbot._internal import main plugins = plugins_disco.PluginsRegistry.find_all() diff --git a/certbot/certbot/_internal/reporter.py b/certbot/certbot/_internal/reporter.py deleted file mode 100644 index fb09b1f27..000000000 --- a/certbot/certbot/_internal/reporter.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Collects and displays information to the user.""" -import collections -import logging -import queue -import sys -import textwrap - -from certbot import configuration -from certbot import util - -logger = logging.getLogger(__name__) - - -class Reporter: - """Collects and displays information to the user. - - :ivar `queue.PriorityQueue` messages: Messages to be displayed to - the user. - - """ - - HIGH_PRIORITY = 0 - """High priority constant. See `add_message`.""" - MEDIUM_PRIORITY = 1 - """Medium priority constant. See `add_message`.""" - LOW_PRIORITY = 2 - """Low priority constant. See `add_message`.""" - - _msg_type = collections.namedtuple('_msg_type', 'priority text on_crash') - - def __init__(self, config: configuration.NamespaceConfig) -> None: - self.messages: "queue.PriorityQueue[Reporter._msg_type]" = queue.PriorityQueue() - self.config = config - - def add_message(self, msg: str, priority: int, on_crash: bool = True) -> None: - """Adds msg to the list of messages to be printed. - - :param str msg: Message to be displayed to the user. - - :param int priority: One of `HIGH_PRIORITY`, `MEDIUM_PRIORITY`, - or `LOW_PRIORITY`. - - :param bool on_crash: Whether or not the message should be - printed if the program exits abnormally. - - """ - assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY - self.messages.put(self._msg_type(priority, msg, on_crash)) - logger.debug("Reporting to user: %s", msg) - - def print_messages(self) -> None: - """Prints messages to the user and clears the message queue. - - If there is an unhandled exception, only messages for which - ``on_crash`` is ``True`` are printed. - - """ - bold_on = False - if not self.messages.empty(): - no_exception = sys.exc_info()[0] is None - bold_on = sys.stdout.isatty() - if not self.config.quiet: - if bold_on: - print(util.ANSI_SGR_BOLD) - print('IMPORTANT NOTES:') - first_wrapper = textwrap.TextWrapper( - initial_indent=' - ', - subsequent_indent=(' ' * 3), - break_long_words=False, - break_on_hyphens=False) - next_wrapper = textwrap.TextWrapper( - initial_indent=first_wrapper.subsequent_indent, - subsequent_indent=first_wrapper.subsequent_indent, - break_long_words=False, - break_on_hyphens=False) - while not self.messages.empty(): - msg = self.messages.get() - if self.config.quiet: - # In --quiet mode, we only print high priority messages that - # are flagged for crash cases - if not (msg.priority == self.HIGH_PRIORITY and msg.on_crash): - continue - if no_exception or msg.on_crash: - if bold_on and msg.priority > self.HIGH_PRIORITY: - if not self.config.quiet: - sys.stdout.write(util.ANSI_SGR_RESET) - bold_on = False - lines = msg.text.splitlines() - print(first_wrapper.fill(lines[0])) - if len(lines) > 1: - print("\n".join( - next_wrapper.fill(line) for line in lines[1:])) - if bold_on and not self.config.quiet: - sys.stdout.write(util.ANSI_SGR_RESET) diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py index 00c37303a..84223b6c0 100644 --- a/certbot/certbot/interfaces.py +++ b/certbot/certbot/interfaces.py @@ -2,19 +2,13 @@ from abc import ABCMeta from abc import abstractmethod from argparse import ArgumentParser -import sys -from types import ModuleType from typing import Any -from typing import Union -from typing import cast from typing import Iterable from typing import List from typing import Optional from typing import Type from typing import TYPE_CHECKING -import warnings - -import zope.interface +from typing import Union from acme.challenges import Challenge from acme.challenges import ChallengeResponse @@ -62,18 +56,6 @@ class AccountStorage(metaclass=ABCMeta): raise NotImplementedError() -class IConfig(zope.interface.Interface): # pylint: disable=inherit-non-class - """Deprecated, use certbot.configuration.NamespaceConfig instead.""" - - -class IPluginFactory(zope.interface.Interface): # pylint: disable=inherit-non-class - """Deprecated, use certbot.interfaces.Plugin as ABC instead.""" - - -class IPlugin(zope.interface.Interface): # pylint: disable=inherit-non-class - """Deprecated, use certbot.interfaces.Plugin as ABC instead.""" - - class Plugin(metaclass=ABCMeta): """Certbot plugin. @@ -168,10 +150,6 @@ class Plugin(metaclass=ABCMeta): """ -class IAuthenticator(IPlugin): # pylint: disable=inherit-non-class - """Deprecated, use certbot.interfaces.Authenticator as ABC instead.""" - - class Authenticator(Plugin): """Generic Certbot Authenticator. @@ -231,10 +209,6 @@ class Authenticator(Plugin): """ -class IInstaller(IPlugin): # pylint: disable=inherit-non-class - """Deprecated, use certbot.interfaces.Installer as ABC instead.""" - - class Installer(Plugin): """Generic Certbot Installer Interface. @@ -362,14 +336,6 @@ class Installer(Plugin): """ -class IDisplay(zope.interface.Interface): # pylint: disable=inherit-non-class - """Deprecated, use your own Display implementation instead.""" - - -class IReporter(zope.interface.Interface): # pylint: disable=inherit-non-class - """Deprecated, use your own Reporter implementation instead.""" - - class RenewableCert(metaclass=ABCMeta): """Interface to a certificate lineage.""" @@ -499,36 +465,3 @@ class RenewDeployer(metaclass=ABCMeta): :type lineage: RenewableCert """ - - -# This class takes a similar approach to the cryptography project to deprecate attributes -# in public modules. See the _ModuleWithDeprecation class here: -# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 -class _ZopeInterfacesDeprecationModule: - """ - Internal class delegating to a module, and displaying warnings when - attributes related to Zope interfaces are accessed. - """ - def __init__(self, module: ModuleType) -> None: - self.__dict__['_module'] = module - - def __getattr__(self, attr: str) -> None: - if attr in ('IConfig', 'IPlugin', 'IPluginFactory', 'IAuthenticator', - 'IInstaller', 'IDisplay', 'IReporter'): - warnings.warn('{0} attribute in certbot.interfaces module is deprecated ' - 'and will be removed soon.'.format(attr), - DeprecationWarning, stacklevel=2) - return getattr(self._module, attr) - - def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover - setattr(self._module, attr, value) - - def __delattr__(self, attr: str) -> None: # pragma: no cover - delattr(self._module, attr) - - def __dir__(self) -> List[str]: # pragma: no cover - return ['_module'] + dir(self._module) - - -# Patching ourselves to warn about Zope interfaces deprecation and planned removal. -sys.modules[__name__] = cast(ModuleType, _ZopeInterfacesDeprecationModule(sys.modules[__name__])) diff --git a/certbot/setup.py b/certbot/setup.py index 8f1de50ca..600087d7f 100644 --- a/certbot/setup.py +++ b/certbot/setup.py @@ -60,8 +60,6 @@ install_requires = [ # installation on Linux. 'pywin32>=300 ; sys_platform == "win32"', f'setuptools>={min_setuptools_version}', - 'zope.component', - 'zope.interface', ] dev_extras = [ diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index 9adbcdfbc..ba0323c3e 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -70,8 +70,7 @@ class HandleAuthorizationsTest(unittest.TestCase): self.mock_display = mock.Mock() self.mock_config = mock.Mock(debug_challenges=False) - with mock.patch("zope.component.provideUtility"): - display_obj.set_display(self.mock_display) + display_obj.set_display(self.mock_display) self.mock_auth = mock.MagicMock(name="Authenticator") diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 514351f32..82138f52d 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -85,7 +85,7 @@ class ParseTest(unittest.TestCase): @staticmethod def parse(*args, **kwargs): - """Mocks zope.component.getUtility and calls _unmocked_parse.""" + """Mocks certbot._internal.display.obj.get_display and calls _unmocked_parse.""" with test_util.patch_display_util(): return ParseTest._unmocked_parse(*args, **kwargs) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index e51be7f9b..70f46aee7 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -70,8 +70,7 @@ class RegisterTest(test_util.ConfigTestCase): self.config.email = "alias@example.com" self.account_storage = account.AccountMemoryStorage() self.tos_cb = mock.MagicMock() - with mock.patch("zope.component.provideUtility"): - display_obj.set_display(MagicMock()) + display_obj.set_display(MagicMock()) def _call(self): from certbot._internal.client import register diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index 0263ff76b..9a111a0eb 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -2,8 +2,6 @@ import logging import unittest -import certbot.util - try: import mock except ImportError: # pragma: no cover diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index ad443f1ca..e26b19357 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1055,9 +1055,7 @@ class MainTest(test_util.ConfigTestCase): plugins.visible().ifaces.assert_called_once_with(ifaces) filtered = plugins.visible().ifaces() self.assertEqual(filtered.init.call_count, 1) - filtered.verify.assert_called_once_with(ifaces) - verified = filtered.verify() - self.assertEqual(stdout.getvalue().strip(), str(verified)) + self.assertEqual(stdout.getvalue().strip(), str(filtered)) @mock.patch('certbot._internal.main.plugins_disco') @mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics') @@ -1073,11 +1071,9 @@ class MainTest(test_util.ConfigTestCase): plugins.visible().ifaces.assert_called_once_with(ifaces) filtered = plugins.visible().ifaces() self.assertEqual(filtered.init.call_count, 1) - filtered.verify.assert_called_once_with(ifaces) - verified = filtered.verify() - verified.prepare.assert_called_once_with() - verified.available.assert_called_once_with() - available = verified.available() + filtered.prepare.assert_called_once_with() + filtered.available.assert_called_once_with() + available = filtered.available() self.assertEqual(stdout.getvalue().strip(), str(available)) def test_certonly_abspath(self): diff --git a/certbot/tests/plugins/disco_test.py b/certbot/tests/plugins/disco_test.py index 7c2dda0da..6b599f561 100644 --- a/certbot/tests/plugins/disco_test.py +++ b/certbot/tests/plugins/disco_test.py @@ -5,7 +5,6 @@ from typing import List import unittest import pkg_resources -import zope.interface from certbot import errors from certbot import interfaces @@ -129,29 +128,6 @@ class PluginEntryPointTest(unittest.TestCase): self.assertIs(self.plugin_ep.misconfigured, False) self.assertIs(self.plugin_ep.available, False) - def test_verify(self): - iface1 = mock.MagicMock(__name__="iface1") - iface2 = mock.MagicMock(__name__="iface2") - iface3 = mock.MagicMock(__name__="iface3") - # pylint: disable=protected-access - self.plugin_ep._initialized = plugin = mock.MagicMock() - - exceptions = zope.interface.exceptions - with mock.patch("certbot._internal.plugins.disco._verify") as mock_verify: - mock_verify.exceptions = exceptions - - def verify_object(obj, cls, iface): # pylint: disable=missing-docstring - assert obj is plugin - assert iface is iface1 or iface is iface2 or iface is iface3 - if iface is iface3: - return False - return True - mock_verify.side_effect = verify_object - self.assertTrue(self.plugin_ep.verify((iface1,))) - self.assertTrue(self.plugin_ep.verify((iface1, iface2))) - self.assertFalse(self.plugin_ep.verify((iface3,))) - self.assertFalse(self.plugin_ep.verify((iface1, iface3))) - def test_prepare(self): config = mock.MagicMock() self.plugin_ep.init(config=config) @@ -264,14 +240,6 @@ class PluginsRegistryTest(unittest.TestCase): self.plugin_ep.ifaces.return_value = False self.assertEqual({}, self.reg.ifaces()._plugins) - def test_verify(self): - self.plugin_ep.verify.return_value = True - # pylint: disable=protected-access - self.assertEqual( - self.plugins, self.reg.verify(mock.MagicMock())._plugins) - self.plugin_ep.verify.return_value = False - self.assertEqual({}, self.reg.verify(mock.MagicMock())._plugins) - def test_prepare(self): self.plugin_ep.prepare.return_value = "baz" self.assertEqual(["baz"], self.reg.prepare()) diff --git a/certbot/tests/plugins/selection_test.py b/certbot/tests/plugins/selection_test.py index fb090f05d..b2f387542 100644 --- a/certbot/tests/plugins/selection_test.py +++ b/certbot/tests/plugins/selection_test.py @@ -77,7 +77,7 @@ class PickPluginTest(unittest.TestCase): plugin_ep.init.return_value = "foo" plugin_ep.misconfigured = False - self.reg.visible().ifaces().verify().available.return_value = { + self.reg.visible().ifaces().available.return_value = { "bar": plugin_ep} self.assertEqual("foo", self._call()) @@ -86,14 +86,14 @@ class PickPluginTest(unittest.TestCase): plugin_ep.init.return_value = "foo" plugin_ep.misconfigured = True - self.reg.visible().ifaces().verify().available.return_value = { + self.reg.visible().ifaces().available.return_value = { "bar": plugin_ep} self.assertIsNone(self._call()) def test_multiple(self): plugin_ep = mock.MagicMock() plugin_ep.init.return_value = "foo" - self.reg.visible().ifaces().verify().available.return_value = { + self.reg.visible().ifaces().available.return_value = { "bar": plugin_ep, "baz": plugin_ep, } @@ -104,7 +104,7 @@ class PickPluginTest(unittest.TestCase): [plugin_ep, plugin_ep], self.question) def test_choose_plugin_none(self): - self.reg.visible().ifaces().verify().available.return_value = { + self.reg.visible().ifaces().available.return_value = { "bar": None, "baz": None, } diff --git a/certbot/tests/reporter_test.py b/certbot/tests/reporter_test.py deleted file mode 100644 index 0270ad3f6..000000000 --- a/certbot/tests/reporter_test.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Tests for certbot._internal.reporter.""" -import io -import sys -import unittest - - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - - -class ReporterTest(unittest.TestCase): - """Tests for certbot._internal.reporter.Reporter.""" - def setUp(self): - from certbot._internal import reporter - self.reporter = reporter.Reporter(mock.MagicMock(quiet=False)) - - self.old_stdout = sys.stdout - sys.stdout = io.StringIO() - - def tearDown(self): - sys.stdout = self.old_stdout - - def test_multiline_message(self): - self.reporter.add_message("Line 1\nLine 2", self.reporter.LOW_PRIORITY) - self.reporter.print_messages() - output = sys.stdout.getvalue() - self.assertIn("Line 1\n", output) - self.assertIn("Line 2", output) - - def test_tty_print_empty(self): - sys.stdout.isatty = lambda: True - self.test_no_tty_print_empty() - - def test_no_tty_print_empty(self): - self.reporter.print_messages() - self.assertEqual(sys.stdout.getvalue(), "") - try: - raise ValueError - except ValueError: - self.reporter.print_messages() - self.assertEqual(sys.stdout.getvalue(), "") - - def test_tty_successful_exit(self): - sys.stdout.isatty = lambda: True - self._successful_exit_common() - - def test_no_tty_successful_exit(self): - self._successful_exit_common() - - def test_tty_unsuccessful_exit(self): - sys.stdout.isatty = lambda: True - self._unsuccessful_exit_common() - - def test_no_tty_unsuccessful_exit(self): - self._unsuccessful_exit_common() - - def _successful_exit_common(self): - self._add_messages() - self.reporter.print_messages() - output = sys.stdout.getvalue() - self.assertIn("IMPORTANT NOTES:", output) - self.assertIn("High", output) - self.assertIn("Med", output) - self.assertIn("Low", output) - - def _unsuccessful_exit_common(self): - self._add_messages() - try: - raise ValueError - except ValueError: - self.reporter.print_messages() - output = sys.stdout.getvalue() - self.assertIn("IMPORTANT NOTES:", output) - self.assertIn("High", output) - self.assertNotIn("Med", output) - self.assertNotIn("Low", output) - - def _add_messages(self): - self.reporter.add_message("High", self.reporter.HIGH_PRIORITY) - self.reporter.add_message( - "Med", self.reporter.MEDIUM_PRIORITY, on_crash=False) - self.reporter.add_message( - "Low", self.reporter.LOW_PRIORITY, on_crash=False) - - -if __name__ == "__main__": - unittest.main() # pragma: no cover diff --git a/pytest.ini b/pytest.ini index 92a403451..4489792b9 100644 --- a/pytest.ini +++ b/pytest.ini @@ -13,25 +13,18 @@ # The current warnings being ignored are: # 1) The warning raised when importing certbot.tests.util and the external mock # library is installed. -# 2) An ImportWarning is raised with older versions of setuptools and -# zope.interface. See -# https://github.com/zopefoundation/zope.interface/issues/68 for more info. -# 3) The deprecation warning raised when importing old Zope interfaces from -# the certbot.interfaces module. -# 4) The deprecation warning raised when importing deprecated attributes from +# 2) The deprecation warning raised when importing deprecated attributes from # the certbot.display.util module. -# 5) A deprecation warning is raised in dnspython==1.15.0 in the oldest tests for +# 3) A deprecation warning is raised in dnspython==1.15.0 in the oldest tests for # certbot-dns-rfc2136. -# 6) The vendored version of six in botocore causes ImportWarnings in Python +# 4) The vendored version of six in botocore causes ImportWarnings in Python # 3.10+. See https://github.com/boto/botocore/issues/2548. -# 7) botocore's default TLS settings raise deprecation warnings in Python +# 5) botocore's default TLS settings raise deprecation warnings in Python # 3.10+, but their values are sane from a security perspective. See # https://github.com/boto/botocore/issues/2550. filterwarnings = error ignore:The external mock module:PendingDeprecationWarning - ignore:.*zope. missing __init__:ImportWarning - ignore:.*attribute in certbot.interfaces module is deprecated:DeprecationWarning ignore:.*attribute in certbot.display.util module is deprecated:DeprecationWarning ignore:decodestring\(\) is a deprecated alias:DeprecationWarning:dns ignore:_SixMetaPathImporter.:ImportWarning diff --git a/tools/oldest_constraints.txt b/tools/oldest_constraints.txt index 90ec56a34..9853143fe 100644 --- a/tools/oldest_constraints.txt +++ b/tools/oldest_constraints.txt @@ -2,9 +2,9 @@ # that script. apacheconfig==0.3.2 asn1crypto==0.24.0 -astroid==2.11.6; python_version >= "3.7" -atomicwrites==1.4.0; sys_platform == "win32" and python_version >= "3.7" -attrs==21.4.0; python_version >= "3.7" +astroid==2.11.7; python_version >= "3.7" +atomicwrites==1.4.1; sys_platform == "win32" and python_version >= "3.7" +attrs==22.1.0; python_version >= "3.7" bcrypt==3.2.2; python_version >= "3.7" boto3==1.15.15 botocore==1.18.15 @@ -16,11 +16,11 @@ cloudflare==1.5.1 colorama==0.4.5; sys_platform == "win32" and python_version >= "3.7" configargparse==0.10.0 configobj==5.0.6 -coverage==6.4.1; python_version >= "3.7" +coverage==6.4.2; python_version >= "3.7" cryptography==3.2.1 -cython==0.29.30 +cython==0.29.31 dill==0.3.5.1; python_version >= "3.7" -distlib==0.3.4; python_version >= "3.7" +distlib==0.3.5; python_version >= "3.7" distro==1.0.1 dns-lexicon==3.2.1 dnspython==1.15.0 @@ -35,7 +35,7 @@ future==0.18.2; python_version >= "3.7" google-api-python-client==1.5.5 httplib2==0.9.2 idna==2.6 -importlib-metadata==4.11.4; python_version < "3.8" and python_version >= "3.7" +importlib-metadata==4.12.0; python_version < "3.8" and python_version >= "3.7" iniconfig==1.1.1; python_version >= "3.7" ipaddress==1.0.16 isort==5.10.1; python_version >= "3.7" and python_version < "4.0" @@ -47,14 +47,14 @@ logger==1.4; python_version >= "3.7" mccabe==0.7.0; python_version >= "3.7" mock==1.0.1 mypy-extensions==0.4.3; python_version >= "3.7" -mypy==0.961; python_version >= "3.7" +mypy==0.971; python_version >= "3.7" ndg-httpsclient==0.3.2 oauth2client==4.0.0 packaging==21.3; python_version >= "3.7" paramiko==2.11.0; python_version >= "3.7" parsedatetime==2.4 pbr==1.8.0 -pip==22.1.2; python_version >= "3.7" +pip==22.2.1; python_version >= "3.7" platformdirs==2.5.2; python_version >= "3.7" pluggy==1.0.0; python_version >= "3.7" ply==3.4 @@ -83,35 +83,31 @@ pyyaml==5.4.1; python_version >= "3.7" requests-file==1.5.1; python_version >= "3.7" requests-toolbelt==0.9.1; python_version >= "3.7" requests==2.20.0 -rsa==4.8; python_version >= "3.7" and python_version < "4" +rsa==4.9; python_version >= "3.7" and python_version < "4" s3transfer==0.3.7; python_version >= "3.7" setuptools==41.6.0 six==1.11.0 texttable==1.6.4; python_version >= "3.7" -tldextract==3.3.0; python_version >= "3.7" +tldextract==3.3.1; python_version >= "3.7" tomli==2.0.1; python_version < "3.11" and python_version >= "3.7" or python_full_version <= "3.11.0a6" and python_version >= "3.7" or python_version >= "3.7" tox==1.9.2; python_version >= "3.7" typed-ast==1.5.4; python_version >= "3.7" and python_version < "3.8" or implementation_name == "cpython" and python_version < "3.8" and python_version >= "3.7" types-cryptography==3.3.21; python_version >= "3.7" types-mock==4.0.15; python_version >= "3.7" -types-pyopenssl==22.0.3; python_version >= "3.7" +types-pyopenssl==22.0.9; python_version >= "3.7" types-pyrfc3339==1.1.1; python_version >= "3.7" -types-python-dateutil==2.8.17; python_version >= "3.7" -types-pytz==2022.1.0; python_version >= "3.7" -types-requests==2.27.31; python_version >= "3.7" -types-setuptools==57.4.17; python_version >= "3.7" -types-six==1.16.16; python_version >= "3.7" -types-urllib3==1.26.15; python_version >= "3.7" -typing-extensions==4.2.0; python_version >= "3.7" or python_version < "3.10" and python_version >= "3.7" or python_version < "3.8" and python_version >= "3.7" +types-python-dateutil==2.8.19; python_version >= "3.7" +types-pytz==2022.1.2; python_version >= "3.7" +types-requests==2.28.5; python_version >= "3.7" +types-setuptools==63.2.2; python_version >= "3.7" +types-six==1.16.18; python_version >= "3.7" +types-urllib3==1.26.17; python_version >= "3.7" +typing-extensions==4.3.0; python_version >= "3.7" or python_version < "3.10" and python_version >= "3.7" or python_version < "3.8" and python_version >= "3.7" uritemplate==3.0.1; python_version >= "3.7" urllib3==1.24.2 -virtualenv==20.14.1; python_version >= "3.7" +virtualenv==20.16.2; python_version >= "3.7" websocket-client==0.59.0; python_version >= "3.7" wheel==0.33.6 wheel==0.33.6; python_version >= "3.7" wrapt==1.14.1; python_version >= "3.7" -zipp==3.8.0; python_version < "3.8" and python_version >= "3.7" -zope.component==4.1.0 -zope.event==4.0.3 -zope.hookable==4.0.4 -zope.interface==4.0.5 +zipp==3.8.1; python_version < "3.8" and python_version >= "3.7" diff --git a/tools/pinning/oldest/pyproject.toml b/tools/pinning/oldest/pyproject.toml index 9b76fdbb5..74b899958 100644 --- a/tools/pinning/oldest/pyproject.toml +++ b/tools/pinning/oldest/pyproject.toml @@ -79,11 +79,6 @@ requests = "2.20.0" setuptools = "41.6.0" six = "1.11.0" urllib3 = "1.24.2" -# Package names containing "." need to be quoted. -"zope.component" = "4.1.0" -"zope.event" = "4.0.3" -"zope.hookable" = "4.0.4" -"zope.interface" = "4.0.5" # Build dependencies # Since there doesn't appear to diff --git a/tools/requirements.txt b/tools/requirements.txt index d57f3a600..71ce482b6 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -189,8 +189,4 @@ websocket-client==0.59.0; python_version >= "3.7" and python_full_version < "3.0 wheel==0.37.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.5.0" wrapt==1.13.3; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version >= "3.5.0" or python_version >= "3.7" and python_full_version >= "3.6.2" yarg==0.1.9; python_version >= "3.7" -zipp==3.7.0; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "3.8" or python_version >= "3.7" and python_version < "3.8" and python_full_version >= "3.5.0" -zope.component==5.0.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -zope.event==4.5.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -zope.hookable==5.1.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -zope.interface==5.4.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" +zipp==3.8.1; python_version >= "3.7" and python_version < "3.8" From c68d4d638994f47e08fb82652ec0493beb265b35 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Wed, 7 Sep 2022 11:08:15 -0700 Subject: [PATCH 23/29] Update changelog for 1.30.0 release --- certbot/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 06a415777..2e3da5f17 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -2,7 +2,7 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). -## 1.30.0 - master +## 1.30.0 - 2022-09-07 ### Added From 667b73687952ed7b6bf1a1f64d34a288f141fb31 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Wed, 7 Sep 2022 11:09:11 -0700 Subject: [PATCH 24/29] Release 1.30.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-dns-cloudflare/setup.py | 2 +- certbot-dns-cloudxns/setup.py | 2 +- certbot-dns-digitalocean/setup.py | 2 +- certbot-dns-dnsimple/setup.py | 2 +- certbot-dns-dnsmadeeasy/setup.py | 2 +- certbot-dns-gehirn/setup.py | 2 +- certbot-dns-google/setup.py | 2 +- certbot-dns-linode/setup.py | 2 +- certbot-dns-luadns/setup.py | 2 +- certbot-dns-nsone/setup.py | 2 +- certbot-dns-ovh/setup.py | 2 +- certbot-dns-rfc2136/setup.py | 2 +- certbot-dns-route53/setup.py | 2 +- certbot-dns-sakuracloud/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/certbot/__init__.py | 2 +- certbot/docs/cli-help.txt | 6 ++++-- 20 files changed, 23 insertions(+), 21 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index f385bcb92..609f34cf8 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -3,7 +3,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'cryptography>=2.5.0', diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 98beb4eaa..5b5f1aeae 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -1,7 +1,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ # We specify the minimum acme and certbot version as the current plugin diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index b34333f6d..fea435842 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -1,7 +1,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 150b9f51e..fa6617fbe 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'cloudflare>=1.5.1', diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index acb3dd4cc..daf743441 100644 --- a/certbot-dns-cloudxns/setup.py +++ b/certbot-dns-cloudxns/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index 1ab16b33f..ab7ba9c0d 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'python-digitalocean>=1.11', # 1.15.0 or newer is recommended for TTL support diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index eb93976d2..c9ac23e5e 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ # This version of lexicon is required to address the problem described in diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index dac8dbb28..bb297be5a 100644 --- a/certbot-dns-dnsmadeeasy/setup.py +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-gehirn/setup.py b/certbot-dns-gehirn/setup.py index f5331d023..36e74a68d 100644 --- a/certbot-dns-gehirn/setup.py +++ b/certbot-dns-gehirn/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index bde460bdc..7e07216ac 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'google-api-python-client>=1.5.5', diff --git a/certbot-dns-linode/setup.py b/certbot-dns-linode/setup.py index 4b9f1e53d..8cc4b147e 100644 --- a/certbot-dns-linode/setup.py +++ b/certbot-dns-linode/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index 5728fc5d7..27c9b9615 100644 --- a/certbot-dns-luadns/setup.py +++ b/certbot-dns-luadns/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index b84ab62bc..7b858a1aa 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-ovh/setup.py b/certbot-dns-ovh/setup.py index 443e2cb25..a73604645 100644 --- a/certbot-dns-ovh/setup.py +++ b/certbot-dns-ovh/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index 207d14182..0b2615d84 100644 --- a/certbot-dns-rfc2136/setup.py +++ b/certbot-dns-rfc2136/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'dnspython>=1.15.0', diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index d09e7e6e2..b030b8ef0 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'boto3>=1.15.15', diff --git a/certbot-dns-sakuracloud/setup.py b/certbot-dns-sakuracloud/setup.py index 60472b280..36acec428 100644 --- a/certbot-dns-sakuracloud/setup.py +++ b/certbot-dns-sakuracloud/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 40809653e..593e083ea 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -1,7 +1,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.30.0.dev0' +version = '1.30.0' install_requires = [ # We specify the minimum acme and certbot version as the current plugin diff --git a/certbot/certbot/__init__.py b/certbot/certbot/__init__.py index 7a59f02f6..e557e206e 100644 --- a/certbot/certbot/__init__.py +++ b/certbot/certbot/__init__.py @@ -1,3 +1,3 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '1.30.0.dev0' +__version__ = '1.30.0' diff --git a/certbot/docs/cli-help.txt b/certbot/docs/cli-help.txt index ca3ba8a4a..8592825f2 100644 --- a/certbot/docs/cli-help.txt +++ b/certbot/docs/cli-help.txt @@ -126,7 +126,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/1.29.0 (certbot; + "". (default: CertbotACMEClient/1.30.0 (certbot; OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY (SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel). The flags encoded in the user agent are: --duplicate, @@ -236,7 +236,9 @@ testing: (default: False) --debug Show tracebacks in case of errors (default: False) --no-verify-ssl Disable verification of the ACME server's certificate. - (default: False) + The root certificates trusted by Certbot can be + overriden by setting the REQUESTS_CA_BUNDLE + environment variable. (default: False) --http-01-port HTTP01_PORT Port used in the http-01 challenge. This only affects the port Certbot listens on. A conforming ACME server From 0b284125d21649e14d572dca37c78384b2f5f1ef Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Wed, 7 Sep 2022 11:09:12 -0700 Subject: [PATCH 25/29] Add contents to certbot/CHANGELOG.md for next version --- certbot/CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 2e3da5f17..8c5f65e50 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -2,6 +2,22 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). +## 1.31.0 - master + +### Added + +* + +### Changed + +* + +### Fixed + +* + +More details about these changes can be found on our GitHub repo. + ## 1.30.0 - 2022-09-07 ### Added From 614eaf68986362b27e76d9bbe092df9afe9affd3 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Wed, 7 Sep 2022 11:09:12 -0700 Subject: [PATCH 26/29] Bump version to 1.31.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-dns-cloudflare/setup.py | 2 +- certbot-dns-cloudxns/setup.py | 2 +- certbot-dns-digitalocean/setup.py | 2 +- certbot-dns-dnsimple/setup.py | 2 +- certbot-dns-dnsmadeeasy/setup.py | 2 +- certbot-dns-gehirn/setup.py | 2 +- certbot-dns-google/setup.py | 2 +- certbot-dns-linode/setup.py | 2 +- certbot-dns-luadns/setup.py | 2 +- certbot-dns-nsone/setup.py | 2 +- certbot-dns-ovh/setup.py | 2 +- certbot-dns-rfc2136/setup.py | 2 +- certbot-dns-route53/setup.py | 2 +- certbot-dns-sakuracloud/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/certbot/__init__.py | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 609f34cf8..5d88564dd 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -3,7 +3,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'cryptography>=2.5.0', diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 5b5f1aeae..425f85db7 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -1,7 +1,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ # We specify the minimum acme and certbot version as the current plugin diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index fea435842..f5c1291ea 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -1,7 +1,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index fa6617fbe..3efbb1494 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'cloudflare>=1.5.1', diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index daf743441..3f298d9be 100644 --- a/certbot-dns-cloudxns/setup.py +++ b/certbot-dns-cloudxns/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index ab7ba9c0d..9b4196133 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'python-digitalocean>=1.11', # 1.15.0 or newer is recommended for TTL support diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index c9ac23e5e..dfc3e529f 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ # This version of lexicon is required to address the problem described in diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index bb297be5a..276a94ca9 100644 --- a/certbot-dns-dnsmadeeasy/setup.py +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-gehirn/setup.py b/certbot-dns-gehirn/setup.py index 36e74a68d..c9247f341 100644 --- a/certbot-dns-gehirn/setup.py +++ b/certbot-dns-gehirn/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index 7e07216ac..d5a6acc94 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'google-api-python-client>=1.5.5', diff --git a/certbot-dns-linode/setup.py b/certbot-dns-linode/setup.py index 8cc4b147e..03f3f3bc2 100644 --- a/certbot-dns-linode/setup.py +++ b/certbot-dns-linode/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index 27c9b9615..224181566 100644 --- a/certbot-dns-luadns/setup.py +++ b/certbot-dns-luadns/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index 7b858a1aa..b89b64e01 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-ovh/setup.py b/certbot-dns-ovh/setup.py index a73604645..70c3d6fbe 100644 --- a/certbot-dns-ovh/setup.py +++ b/certbot-dns-ovh/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index 0b2615d84..01a026d61 100644 --- a/certbot-dns-rfc2136/setup.py +++ b/certbot-dns-rfc2136/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'dnspython>=1.15.0', diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index b030b8ef0..ed4cc0a11 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'boto3>=1.15.15', diff --git a/certbot-dns-sakuracloud/setup.py b/certbot-dns-sakuracloud/setup.py index 36acec428..10a606b4c 100644 --- a/certbot-dns-sakuracloud/setup.py +++ b/certbot-dns-sakuracloud/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 593e083ea..4a4d57509 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -1,7 +1,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.30.0' +version = '1.31.0.dev0' install_requires = [ # We specify the minimum acme and certbot version as the current plugin diff --git a/certbot/certbot/__init__.py b/certbot/certbot/__init__.py index e557e206e..27b94a930 100644 --- a/certbot/certbot/__init__.py +++ b/certbot/certbot/__init__.py @@ -1,3 +1,3 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '1.30.0' +__version__ = '1.31.0.dev0' From 39e8d14e1b221bf37526cc05ecc83beee30a3c57 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 9 Sep 2022 14:23:39 -0700 Subject: [PATCH 27/29] Set up 2.0 pre-releases (#9400) * update credential info * update release tooling to use candidate channel * split deploy jobs * pass parameter through * add 2.0 pipeline prerelease * add comments * quote file path --- .azure-pipelines/2.0-prerelease.yml | 18 +++++ .azure-pipelines/release.yml | 2 +- .../templates/jobs/snap-deploy-job.yml | 75 +++++++++++++++++++ .../templates/stages/deploy-stage.yml | 68 ++--------------- tools/finish_release.py | 10 +-- 5 files changed, 104 insertions(+), 69 deletions(-) create mode 100644 .azure-pipelines/2.0-prerelease.yml create mode 100644 .azure-pipelines/templates/jobs/snap-deploy-job.yml diff --git a/.azure-pipelines/2.0-prerelease.yml b/.azure-pipelines/2.0-prerelease.yml new file mode 100644 index 000000000..2cdcf8f30 --- /dev/null +++ b/.azure-pipelines/2.0-prerelease.yml @@ -0,0 +1,18 @@ +# Pipeline for testing, building, and deploying Certbot 2.0 pre-releases. +trigger: none +pr: none + +variables: + # We don't publish our Docker images in this pipeline, but when building them + # for testing, let's use the nightly tag. + dockerTag: nightly + snapBuildTimeout: 5400 + +stages: + - template: templates/stages/test-and-package-stage.yml + - stage: DeploySnaps + jobs: + - template: templates/jobs/snap-deploy-job.yml + parameters: + snapReleaseChannel: beta + - template: templates/stages/notify-failure-stage.yml diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 26639151f..9169dc950 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -15,5 +15,5 @@ stages: - template: templates/stages/changelog-stage.yml - template: templates/stages/deploy-stage.yml parameters: - snapReleaseChannel: beta + snapReleaseChannel: candidate - template: templates/stages/notify-failure-stage.yml diff --git a/.azure-pipelines/templates/jobs/snap-deploy-job.yml b/.azure-pipelines/templates/jobs/snap-deploy-job.yml new file mode 100644 index 000000000..d1d709cb9 --- /dev/null +++ b/.azure-pipelines/templates/jobs/snap-deploy-job.yml @@ -0,0 +1,75 @@ +# As (somewhat) described at +# https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops#context, +# each template only has access to the parameters passed into it. To help make +# use of this design, we define snapReleaseChannel without a default value +# which requires the user of this template to define it as described at +# https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/parameters-name?view=azure-pipelines#remarks. +# This makes the user of this template be explicit while allowing them to +# define their own parameters with defaults that make sense for that context. +parameters: +- name: snapReleaseChannel + type: string + values: + - edge + - beta + - candidate + +jobs: + # This job relies on credentials used to publish the Certbot snaps. This + # credential file was created by running: + # + # snapcraft logout + # snapcraft export-login --channels=candidate,beta,edge snapcraft.cfg + # (provide the shared snapcraft credentials when prompted) + # + # Then the file was added as a secure file in Azure pipelines + # with the name snapcraft.cfg by following the instructions at + # https://docs.microsoft.com/en-us/azure/devops/pipelines/library/secure-files?view=azure-devops + # including authorizing the file for use in the "nightly" and "release" + # pipelines as described at + # https://docs.microsoft.com/en-us/azure/devops/pipelines/library/secure-files?view=azure-devops#q-how-do-i-authorize-a-secure-file-for-use-in-a-specific-pipeline. + # + # This file has a maximum lifetime of one year and the current file will + # expire on 2023-09-06. The file will need to be updated before then to + # prevent automated deploys from breaking. + # + # Revoking these credentials can be done by changing the password of the + # account used to generate the credentials. See + # https://forum.snapcraft.io/t/revoking-exported-credentials/19031 for + # more info. + - job: publish_snap + pool: + vmImage: ubuntu-22.04 + variables: + - group: certbot-common + strategy: + matrix: + amd64: + SNAP_ARCH: amd64 + arm32v6: + SNAP_ARCH: armhf + arm64v8: + SNAP_ARCH: arm64 + steps: + - bash: | + set -e + sudo apt-get update + sudo apt-get install -y --no-install-recommends snapd + sudo snap install --classic snapcraft + displayName: Install dependencies + - task: DownloadPipelineArtifact@2 + inputs: + artifact: snaps_$(SNAP_ARCH) + path: $(Build.SourcesDirectory)/snap + displayName: Retrieve Certbot snaps + - task: DownloadSecureFile@1 + name: snapcraftCfg + inputs: + secureFile: snapcraft.cfg + - bash: | + set -e + export SNAPCRAFT_STORE_CREDENTIALS=$(cat "$(snapcraftCfg.secureFilePath)") + for SNAP_FILE in snap/*.snap; do + tools/retry.sh eval snapcraft upload --release=${{ parameters.snapReleaseChannel }} "${SNAP_FILE}" + done + displayName: Publish to Snap store diff --git a/.azure-pipelines/templates/stages/deploy-stage.yml b/.azure-pipelines/templates/stages/deploy-stage.yml index 8144f4bee..cb8b24e28 100644 --- a/.azure-pipelines/templates/stages/deploy-stage.yml +++ b/.azure-pipelines/templates/stages/deploy-stage.yml @@ -1,74 +1,16 @@ parameters: +# We do not define acceptable values for this parameter here as it is passed +# through to ../jobs/snap-deploy-job.yml which does its own sanity checking. - name: snapReleaseChannel type: string default: edge - values: - - edge - - beta stages: - stage: Deploy jobs: - # This job relies on credentials used to publish the Certbot snaps. This - # credential file was created by running: - # - # snapcraft logout - # snapcraft export-login --channels=beta,edge snapcraft.cfg - # (provide the shared snapcraft credentials when prompted) - # - # Then the file was added as a secure file in Azure pipelines - # with the name snapcraft.cfg by following the instructions at - # https://docs.microsoft.com/en-us/azure/devops/pipelines/library/secure-files?view=azure-devops - # including authorizing the file for use in the "nightly" and "release" - # pipelines as described at - # https://docs.microsoft.com/en-us/azure/devops/pipelines/library/secure-files?view=azure-devops#q-how-do-i-authorize-a-secure-file-for-use-in-a-specific-pipeline. - # - # This file has a maximum lifetime of one year and the current - # file will expire on 2023-06-17 which is also tracked by - # https://github.com/certbot/certbot/issues/7931. The file will - # need to be updated before then to prevent automated deploys - # from breaking. - # - # Revoking these credentials can be done by changing the password of the - # account used to generate the credentials. See - # https://forum.snapcraft.io/t/revoking-exported-credentials/19031 for - # more info. - - job: publish_snap - pool: - vmImage: ubuntu-22.04 - variables: - - group: certbot-common - strategy: - matrix: - amd64: - SNAP_ARCH: amd64 - arm32v6: - SNAP_ARCH: armhf - arm64v8: - SNAP_ARCH: arm64 - steps: - - bash: | - set -e - sudo apt-get update - sudo apt-get install -y --no-install-recommends snapd - sudo snap install --classic snapcraft - displayName: Install dependencies - - task: DownloadPipelineArtifact@2 - inputs: - artifact: snaps_$(SNAP_ARCH) - path: $(Build.SourcesDirectory)/snap - displayName: Retrieve Certbot snaps - - task: DownloadSecureFile@1 - name: snapcraftCfg - inputs: - secureFile: snapcraft.cfg - - bash: | - set -e - export SNAPCRAFT_STORE_CREDENTIALS=$(cat $(snapcraftCfg.secureFilePath)) - for SNAP_FILE in snap/*.snap; do - tools/retry.sh eval snapcraft upload --release=${{ parameters.snapReleaseChannel }} "${SNAP_FILE}" - done - displayName: Publish to Snap store + - template: ../jobs/snap-deploy-job.yml + parameters: + snapReleaseChannel: ${{ parameters.snapReleaseChannel }} - job: publish_docker pool: vmImage: ubuntu-22.04 diff --git a/tools/finish_release.py b/tools/finish_release.py index ec749d48f..18aa8ee30 100755 --- a/tools/finish_release.py +++ b/tools/finish_release.py @@ -4,7 +4,7 @@ Post-release script to publish artifacts created from Azure Pipelines. This currently includes: -* Moving snaps from the beta channel to the stable channel +* Moving snaps from the candidate channel to the stable channel * Publishing the Windows installer in a GitHub release Setup: @@ -110,7 +110,7 @@ def assert_logged_into_snapcraft(): def get_snap_revisions(snap, version): - """Finds the revisions for the snap and version in the beta channel. + """Finds the revisions for the snap and version in the candidate channel. If you call this function without being logged in with snapcraft, it will hang with no output. @@ -130,20 +130,20 @@ def get_snap_revisions(snap, version): print('Getting revision numbers for', snap, version) cmd = ['snapcraft', 'status', snap] process = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, universal_newlines=True) - pattern = f'^\s+beta\s+{version}\s+(\d+)\s*' + pattern = f'^\s+candidate\s+{version}\s+(\d+)\s*' revisions = re.findall(pattern, process.stdout, re.MULTILINE) assert len(revisions) == SNAP_ARCH_COUNT, f'Unexpected number of snaps found for {snap} {version} (expected {SNAP_ARCH_COUNT}, found {len(revisions)})' return revisions def promote_snaps(version): - """Promotes all Certbot snaps from the beta to stable channel. + """Promotes all Certbot snaps from the candidate to stable channel. If the snaps have already been released to the stable channel, this function will try to release them again which has no effect. :param str version: the version number that should be found in the - beta channel, e.g. 1.7.0 + candidate channel, e.g. 1.7.0 :raises SystemExit: if the command snapcraft is unavailable or it isn't logged into an account From c42dd567cad25a6baebbbd83ad3cff4e0220d87a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 26 Sep 2022 19:30:05 -0700 Subject: [PATCH 28/29] remove source_address arg (#9418) --- acme/acme/client.py | 19 +------------------ acme/setup.py | 1 - acme/tests/client_test.py | 27 --------------------------- 3 files changed, 1 insertion(+), 46 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index a7471a4ec..17ff96e06 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -15,20 +15,12 @@ from typing import Set from typing import Text from typing import Tuple from typing import Union -import warnings import josepy as jose import OpenSSL import requests from requests.adapters import HTTPAdapter from requests.utils import parse_header_links -# We're capturing the warnings described at -# https://github.com/requests/toolbelt/issues/331 until we can remove this -# dependency in Certbot 2.0. -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", "'urllib3.contrib.pyopenssl", - DeprecationWarning) - from requests_toolbelt.adapters.source import SourceAddressAdapter from acme import challenges from acme import crypto_util @@ -489,14 +481,10 @@ class ClientNetwork: :param bool verify_ssl: Whether to verify certificates on SSL connections. :param str user_agent: String to send as User-Agent header. :param float timeout: Timeout for requests. - :param source_address: Optional source address to bind to when making - requests. (deprecated since 1.30.0) - :type source_address: str or tuple(str, int) """ def __init__(self, key: jose.JWK, account: Optional[messages.RegistrationResource] = None, alg: jose.JWASignature = jose.RS256, verify_ssl: bool = True, - user_agent: str = 'acme-python', timeout: int = DEFAULT_NETWORK_TIMEOUT, - source_address: Optional[Union[str, Tuple[str, int]]] = None) -> None: + user_agent: str = 'acme-python', timeout: int = DEFAULT_NETWORK_TIMEOUT) -> None: self.key = key self.account = account self.alg = alg @@ -507,11 +495,6 @@ class ClientNetwork: self._default_timeout = timeout adapter = HTTPAdapter() - if source_address is not None: - warnings.warn("Support for source_address is deprecated and will be " - "removed soon.", DeprecationWarning, stacklevel=2) - adapter = SourceAddressAdapter(source_address) - self.session.mount("http://", adapter) self.session.mount("https://", adapter) diff --git a/acme/setup.py b/acme/setup.py index 5d88564dd..d71580153 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -12,7 +12,6 @@ install_requires = [ 'pyrfc3339', 'pytz>=2019.3', 'requests>=2.20.0', - 'requests-toolbelt>=0.3.0', 'setuptools>=41.6.0', ] diff --git a/acme/tests/client_test.py b/acme/tests/client_test.py index c3209685c..093ac519a 100644 --- a/acme/tests/client_test.py +++ b/acme/tests/client_test.py @@ -791,32 +791,5 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): self.net.post('uri', self.obj, content_type=None, new_nonce_url='new_nonce_uri') -class ClientNetworkSourceAddressBindingTest(unittest.TestCase): - """Tests that if ClientNetwork has a source IP set manually, the underlying library has - used the provided source address.""" - - def setUp(self): - self.source_address = "8.8.8.8" - - def test_source_address_set(self): - with mock.patch('warnings.warn') as mock_warn: - net = ClientNetwork(key=None, alg=None, source_address=self.source_address) - mock_warn.assert_called_once() - self.assertIn('source_address', mock_warn.call_args[0][0]) - for adapter in net.session.adapters.values(): - self.assertIn(self.source_address, adapter.source_address) - - def test_behavior_assumption(self): - """This is a test that guardrails the HTTPAdapter behavior so that if the default for - a Session() changes, the assumptions here aren't violated silently.""" - # Source address not specified, so the default adapter type should be bound -- this - # test should fail if the default adapter type is changed by requests - net = ClientNetwork(key=None, alg=None) - session = requests.Session() - for scheme in session.adapters: - client_network_adapter = net.session.adapters.get(scheme) - default_adapter = session.adapters.get(scheme) - self.assertEqual(client_network_adapter.__class__, default_adapter.__class__) - if __name__ == '__main__': unittest.main() # pragma: no cover From 212c2ba990758cb9acd2b200e55302534988089a Mon Sep 17 00:00:00 2001 From: alexzorin Date: Tue, 27 Sep 2022 12:37:24 +1000 Subject: [PATCH 29/29] error out when --reuse-key conflicts with other flags (#9262) * error out when --reuse-key conflicts with other flags * add unit test * add integration tests * lint --- .../certbot_tests/assertions.py | 4 +- .../certbot_tests/test_main.py | 13 +++++ certbot/certbot/_internal/renewal.py | 45 +++++++++++++++++ certbot/certbot/_internal/storage.py | 49 ++++++++++++++++--- certbot/tests/main_test.py | 1 + certbot/tests/renewal_test.py | 42 ++++++++++++++-- 6 files changed, 141 insertions(+), 13 deletions(-) diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py b/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py index 3563b30af..a1e814405 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py @@ -33,8 +33,8 @@ def assert_elliptic_key(key: str, curve: Type[EllipticCurve]) -> None: key = load_pem_private_key(data=privkey1, password=None, backend=default_backend()) - assert isinstance(key, EllipticCurvePrivateKey) - assert isinstance(key.curve, curve) + assert isinstance(key, EllipticCurvePrivateKey), f"should be an EC key but was {type(key)}" + assert isinstance(key.curve, curve), f"should have curve {curve} but was {key.curve}" def assert_rsa_key(key: str, key_size: Optional[int] = None) -> None: diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py index 7bef646be..64ec45178 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py @@ -507,6 +507,19 @@ def test_new_key(context: IntegrationTestsContext) -> None: assert_saved_lineage_option(context.config_dir, certname, 'reuse_key', 'True') assert_elliptic_key(privkey4_path, SECP256R1) + # certonly: it should not be possible to change a key parameter without --new-key + with pytest.raises(subprocess.CalledProcessError) as error: + context.certbot(['certonly', '-d', certname, '--reuse-key', + '--elliptic-curve', 'secp384r1']) + assert 'Unable to change the --elliptic-curve' in error.value.stderr + + # certonly: not specifying --key-type should keep the existing key type (non-interactively). + # TODO: when ECDSA is made default key type, the key types must be inverted + context.certbot(['certonly', '-d', certname, '--no-reuse-key']) + privkey5, privkey5_path = private_key(5) + assert_elliptic_key(privkey5_path, SECP256R1) + assert privkey4 != privkey5 + def test_incorrect_key_type(context: IntegrationTestsContext) -> None: with pytest.raises(subprocess.CalledProcessError): diff --git a/certbot/certbot/_internal/renewal.py b/certbot/certbot/_internal/renewal.py index bd84901e1..35f96a14f 100644 --- a/certbot/certbot/_internal/renewal.py +++ b/certbot/certbot/_internal/renewal.py @@ -324,12 +324,57 @@ def _avoid_invalidating_lineage(config: configuration.NamespaceConfig, "unless you use the --break-my-certs flag!") +def _avoid_reuse_key_conflicts(config: configuration.NamespaceConfig, + lineage: storage.RenewableCert) -> None: + """Don't allow combining --reuse-key with any flags that would conflict + with key reuse (--key-type, --rsa-key-size, --elliptic-curve), unless + --new-key is also set. + """ + # If --no-reuse-key is set, no conflict + if cli.set_by_cli("reuse_key") and not config.reuse_key: + return + + # If reuse_key is not set on the lineage and --reuse-key is not + # set on the CLI, no conflict. + if not lineage.reuse_key and not config.reuse_key: + return + + # If --new-key is set, no conflict + if config.new_key: + return + + kt = config.key_type.lower() + + # The remaining cases where conflicts are present: + # - --key-type is set on the CLI and doesn't match the stored private key + # - It's an RSA key and --rsa-key-size is set and doesn't match + # - It's an ECDSA key and --eliptic-curve is set and doesn't match + potential_conflicts = [ + ("--key-type", + lambda: kt != lineage.private_key_type.lower()), + ("--rsa-key-type", + lambda: kt == "rsa" and config.rsa_key_size != lineage.rsa_key_size), + ("--elliptic-curve", + lambda: kt == "ecdsa" and lineage.elliptic_curve and \ + config.elliptic_curve.lower() != lineage.elliptic_curve.lower()) + ] + + for conflict in potential_conflicts: + if conflict[1](): + raise errors.Error( + f"Unable to change the {conflict[0]} of this certificate because --reuse-key " + "is set. To stop reusing the private key, specify --no-reuse-key. " + "To change the private key this one time and then reuse it in future, " + "add --new-key.") + + def renew_cert(config: configuration.NamespaceConfig, domains: Optional[List[str]], le_client: client.Client, lineage: storage.RenewableCert) -> None: """Renew a certificate lineage.""" renewal_params = lineage.configuration["renewalparams"] original_server = renewal_params.get("server", cli.flag_default("server")) _avoid_invalidating_lineage(config, lineage, original_server) + _avoid_reuse_key_conflicts(config, lineage) if not domains: domains = lineage.names() # The private key is the existing lineage private key if reuse_key is set. diff --git a/certbot/certbot/_internal/storage.py b/certbot/certbot/_internal/storage.py index 567073acf..978295cce 100644 --- a/certbot/certbot/_internal/storage.py +++ b/certbot/certbot/_internal/storage.py @@ -12,10 +12,12 @@ from typing import List from typing import Mapping from typing import Optional from typing import Tuple +from typing import Union import configobj from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey from cryptography.hazmat.primitives.serialization import load_pem_private_key import parsedatetime import pkg_resources @@ -569,6 +571,12 @@ class RenewableCert(interfaces.RenewableCert): return util.is_staging(self.server) return False + @property + def reuse_key(self) -> bool: + """Returns whether this certificate is configured to reuse its private key""" + return "reuse_key" in self.configuration["renewalparams"] and \ + self.configuration["renewalparams"].as_bool("reuse_key") + def _check_symlinks(self) -> None: """Raises an exception if a symlink doesn't exist""" for kind in ALL_FOUR: @@ -1115,22 +1123,47 @@ class RenewableCert(interfaces.RenewableCert): target, values) return cls(new_config.filename, cli_config) - @property - def private_key_type(self) -> str: - """ - :returns: The type of algorithm for the private, RSA or ECDSA - :rtype: str - """ + def _private_key(self) -> Union[RSAPrivateKey, EllipticCurvePrivateKey]: with open(self.configuration["privkey"], "rb") as priv_key_file: key = load_pem_private_key( data=priv_key_file.read(), password=None, backend=default_backend() ) + return key + + @property + def private_key_type(self) -> str: + """ + :returns: The type of algorithm for the private, RSA or ECDSA + :rtype: str + """ + key = self._private_key() if isinstance(key, RSAPrivateKey): return "RSA" - else: - return "ECDSA" + return "ECDSA" + + @property + def rsa_key_size(self) -> Optional[int]: + """ + :returns: If the private key is an RSA key, its size. + :rtype: int + """ + key = self._private_key() + if isinstance(key, RSAPrivateKey): + return key.key_size + return None + + @property + def elliptic_curve(self) -> Optional[str]: + """ + :returns: If the private key is an elliptic key, the name of its curve. + :rtype: str + """ + key = self._private_key() + if isinstance(key, EllipticCurvePrivateKey): + return key.curve.name + return None def save_successor(self, prior_version: int, new_cert: bytes, new_privkey: bytes, new_chain: bytes, cli_config: configuration.NamespaceConfig) -> int: diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index e26b19357..b2723ced3 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1209,6 +1209,7 @@ class MainTest(test_util.ConfigTestCase): mock_lineage.has_pending_deployment.return_value = False mock_lineage.names.return_value = ['isnot.org'] mock_lineage.private_key_type = 'RSA' + mock_lineage.rsa_key_size = 2048 mock_certr = mock.MagicMock() mock_key = mock.MagicMock(pem='pem_key') mock_client = mock.MagicMock() diff --git a/certbot/tests/renewal_test.py b/certbot/tests/renewal_test.py index d6e2866dc..a33332e30 100644 --- a/certbot/tests/renewal_test.py +++ b/certbot/tests/renewal_test.py @@ -55,7 +55,8 @@ class RenewalTest(test_util.ConfigTestCase): self.assertEqual(self.config.webroot_map, {}) self.assertEqual(self.config.webroot_path, ['/var/www/test']) - def test_reuse_key_renewal_params(self): + @mock.patch('certbot._internal.renewal._avoid_reuse_key_conflicts') + def test_reuse_key_renewal_params(self, unused_mock_avoid_reuse_conflicts): self.config.rsa_key_size = 'INVALID_VALUE' self.config.reuse_key = True self.config.dry_run = True @@ -75,7 +76,8 @@ class RenewalTest(test_util.ConfigTestCase): assert self.config.rsa_key_size == 2048 - def test_reuse_ec_key_renewal_params(self): + @mock.patch('certbot._internal.renewal._avoid_reuse_key_conflicts') + def test_reuse_ec_key_renewal_params(self, unused_mock_avoid_reuse_conflicts): self.config.elliptic_curve = 'INVALID_CURVE' self.config.reuse_key = True self.config.dry_run = True @@ -99,7 +101,9 @@ class RenewalTest(test_util.ConfigTestCase): assert self.config.elliptic_curve == 'secp256r1' - def test_new_key(self): + @mock.patch('certbot._internal.renewal.cli.set_by_cli') + def test_new_key(self, mock_set_by_cli): + mock_set_by_cli.return_value = False # When renewing with both reuse_key and new_key, the key should be regenerated, # the key type, key parameters and reuse_key should be kept. self.config.reuse_key = True @@ -125,6 +129,38 @@ class RenewalTest(test_util.ConfigTestCase): # None is passed as the existing key, i.e. the key is not actually being reused. le_client.obtain_certificate.assert_called_with(mock.ANY, None) + @mock.patch('certbot._internal.renewal.hooks.renew_hook') + @mock.patch('certbot._internal.renewal.cli.set_by_cli') + def test_reuse_key_conflicts(self, mock_set_by_cli, unused_mock_renew_hook): + mock_set_by_cli.return_value = False + + # When renewing with reuse_key and a conflicting key parameter (size, curve) + # an error should be raised ... + self.config.reuse_key = True + self.config.key_type = "rsa" + self.config.rsa_key_size = 4096 + self.config.dry_run = True + + config = configuration.NamespaceConfig(self.config) + + rc_path = test_util.make_lineage( + self.config.config_dir, 'sample-renewal.conf') + lineage = storage.RenewableCert(rc_path, config) + lineage.configuration["renewalparams"]["reuse_key"] = True + + le_client = mock.MagicMock() + le_client.obtain_certificate.return_value = (None, None, None, None) + + from certbot._internal import renewal + + with self.assertRaisesRegex(errors.Error, "Unable to change the --rsa-key-type"): + renewal.renew_cert(self.config, None, le_client, lineage) + + # ... unless --no-reuse-key is set + mock_set_by_cli.side_effect = lambda var: var == "reuse_key" + self.config.reuse_key = False + renewal.renew_cert(self.config, None, le_client, lineage) + @test_util.patch_display_util() @mock.patch('certbot._internal.renewal.cli.set_by_cli') def test_remove_deprecated_config_elements(self, mock_set_by_cli, unused_mock_get_utility):