Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-certbot-dns-cloudflare for
openSUSE:Factory checked in at 2025-06-16 12:26:25
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-certbot-dns-cloudflare (Old)
and /work/SRC/openSUSE:Factory/.python-certbot-dns-cloudflare.new.19631
(New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-certbot-dns-cloudflare"
Mon Jun 16 12:26:25 2025 rev:45 rq:1286008 version:4.1.1
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-certbot-dns-cloudflare/python-certbot-dns-cloudflare.changes
2025-04-22 17:29:43.734725420 +0200
+++
/work/SRC/openSUSE:Factory/.python-certbot-dns-cloudflare.new.19631/python-certbot-dns-cloudflare.changes
2025-06-16 12:26:27.068200320 +0200
@@ -1,0 +2,7 @@
+Fri Jun 13 14:50:42 UTC 2025 - Markéta Machová <[email protected]>
+
+- update to version 4.1.1
+ * Switched to src-layout from flat-layout to accommodate PEP 517 pip
+ editable installs
+
+-------------------------------------------------------------------
Old:
----
certbot_dns_cloudflare-4.0.0.tar.gz
New:
----
certbot_dns_cloudflare-4.1.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-certbot-dns-cloudflare.spec ++++++
--- /var/tmp/diff_new_pack.sJarsl/_old 2025-06-16 12:26:27.668225305 +0200
+++ /var/tmp/diff_new_pack.sJarsl/_new 2025-06-16 12:26:27.668225305 +0200
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-certbot-dns-cloudflare
-Version: 4.0.0
+Version: 4.1.1
Release: 0
Summary: Cloudflare Authenticator plugin for Certbot
License: Apache-2.0
++++++ certbot_dns_cloudflare-4.0.0.tar.gz ->
certbot_dns_cloudflare-4.1.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/certbot_dns_cloudflare-4.0.0/MANIFEST.in
new/certbot_dns_cloudflare-4.1.1/MANIFEST.in
--- old/certbot_dns_cloudflare-4.0.0/MANIFEST.in 2025-04-08
00:03:33.000000000 +0200
+++ new/certbot_dns_cloudflare-4.1.1/MANIFEST.in 2025-06-12
20:08:34.000000000 +0200
@@ -1,6 +1,6 @@
include LICENSE.txt
include README.rst
recursive-include docs *
-include certbot_dns_cloudflare/py.typed
+include src/certbot_dns_cloudflare/py.typed
global-exclude __pycache__
global-exclude *.py[cod]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/certbot_dns_cloudflare-4.0.0/PKG-INFO
new/certbot_dns_cloudflare-4.1.1/PKG-INFO
--- old/certbot_dns_cloudflare-4.0.0/PKG-INFO 2025-04-08 00:03:36.605529000
+0200
+++ new/certbot_dns_cloudflare-4.1.1/PKG-INFO 2025-06-12 20:08:38.535003200
+0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: certbot-dns-cloudflare
-Version: 4.0.0
+Version: 4.1.1
Summary: Cloudflare DNS Authenticator plugin for Certbot
Home-page: https://github.com/certbot/certbot
Author: Certbot Project
@@ -24,11 +24,11 @@
Classifier: Topic :: System :: Networking
Classifier: Topic :: System :: Systems Administration
Classifier: Topic :: Utilities
-Requires-Python: >=3.9
+Requires-Python: >=3.9.2
License-File: LICENSE.txt
Requires-Dist: cloudflare<2.20,>=2.19
-Requires-Dist: acme>=4.0.0
-Requires-Dist: certbot>=4.0.0
+Requires-Dist: acme>=4.1.1
+Requires-Dist: certbot>=4.1.1
Provides-Extra: docs
Requires-Dist: Sphinx>=1.0; extra == "docs"
Requires-Dist: sphinx_rtd_theme; extra == "docs"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/__init__.py
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/__init__.py
--- old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/__init__.py
2025-04-08 00:03:33.000000000 +0200
+++ new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/__init__.py
1970-01-01 01:00:00.000000000 +0100
@@ -1,124 +0,0 @@
-"""
-The `~certbot_dns_cloudflare.dns_cloudflare` plugin automates the process of
-completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
-subsequently removing, TXT records using the Cloudflare API.
-
-.. note::
- The plugin is not installed by default. It can be installed by heading to
- `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_,
choosing your system and
- selecting the Wildcard tab.
-
-Named Arguments
----------------
-
-======================================== =====================================
-``--dns-cloudflare-credentials`` Cloudflare credentials_ INI file.
- (Required)
-``--dns-cloudflare-propagation-seconds`` The number of seconds to wait for DNS
- to propagate before asking the ACME
- server to verify the DNS record.
- (Default: 10)
-======================================== =====================================
-
-
-Credentials
------------
-
-Use of this plugin requires a configuration file containing Cloudflare API
-credentials, obtained from your
-`Cloudflare dashboard
<https://dash.cloudflare.com/?to=/:account/profile/api-tokens>`_.
-
-Previously, Cloudflare's "Global API Key" was used for authentication, however
-this key can access the entire Cloudflare API for all domains in your account,
-meaning it could cause a lot of damage if leaked.
-
-Cloudflare's newer API Tokens can be restricted to specific domains and
-operations, and are therefore now the recommended authentication option.
-
-The Token needed by Certbot requires ``Zone:DNS:Edit`` permissions for only the
-zones you need certificates for.
-
-Using Cloudflare Tokens also requires at least version 2.3.1 of the
``cloudflare``
-Python module. If the version that automatically installed with this plugin is
-older than that, and you can't upgrade it on your system, you'll have to stick
to
-the Global key.
-
-.. code-block:: ini
- :name: certbot_cloudflare_token.ini
- :caption: Example credentials file using restricted API Token (recommended):
-
- # Cloudflare API token used by Certbot
- dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567
-
-.. code-block:: ini
- :name: certbot_cloudflare_key.ini
- :caption: Example credentials file using Global API Key (not recommended):
-
- # Cloudflare API credentials used by Certbot
- dns_cloudflare_email = [email protected]
- dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234
-
-The path to this file can be provided interactively or using the
-``--dns-cloudflare-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
- Cloudflare 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).
-
-.. note::
- Please note that the ``cloudflare`` Python module used by the plugin has
- additional methods of providing credentials to the module, e.g. environment
- variables or the ``cloudflare.cfg`` configuration file. These methods are
not
- supported by Certbot. If any of those additional methods of providing
- credentials is being used, they must provide the same credentials (i.e.,
- email and API key *or* an API token) as the credentials file provided to
- Certbot. If there is a discrepancy, the ``cloudflare`` Python module will
- raise an error. Also note that the credentials provided to Certbot will
take
- precedence over any other method of providing credentials to the
``cloudflare``
- Python module.
-
-
-Examples
---------
-
-.. code-block:: bash
- :caption: To acquire a certificate for ``example.com``
-
- certbot certonly \\
- --dns-cloudflare \\
- --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\
- -d example.com
-
-.. code-block:: bash
- :caption: To acquire a single certificate for both ``example.com`` and
- ``www.example.com``
-
- certbot certonly \\
- --dns-cloudflare \\
- --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.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-cloudflare \\
- --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\
- --dns-cloudflare-propagation-seconds 60 \\
- -d example.com
-
-"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/__init__.py
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/__init__.py
---
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/__init__.py
2025-04-08 00:03:33.000000000 +0200
+++
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/__init__.py
1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-"""Internal implementation of `~certbot_dns_cloudflare.dns_cloudflare`
plugin."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/dns_cloudflare.py
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/dns_cloudflare.py
---
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/dns_cloudflare.py
2025-04-08 00:03:33.000000000 +0200
+++
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/dns_cloudflare.py
1970-01-01 01:00:00.000000000 +0100
@@ -1,273 +0,0 @@
-"""DNS Authenticator for Cloudflare."""
-import logging
-from typing import Any
-from typing import Callable
-from typing import Dict
-from typing import List
-from typing import Optional
-from typing import cast
-
-import CloudFlare
-
-from certbot import errors
-from certbot.plugins import dns_common
-from certbot.plugins.dns_common import CredentialsConfiguration
-
-logger = logging.getLogger(__name__)
-
-ACCOUNT_URL = 'https://dash.cloudflare.com/?to=/:account/profile/api-tokens'
-
-
-class Authenticator(dns_common.DNSAuthenticator):
- """DNS Authenticator for Cloudflare
-
- This Authenticator uses the Cloudflare API to fulfill a dns-01 challenge.
- """
-
- description = ('Obtain certificates using a DNS TXT record (if you are
using Cloudflare for '
- 'DNS).')
- ttl = 120
-
- 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 = 10) -> None:
- super().add_parser_arguments(add, default_propagation_seconds)
- add('credentials', help='Cloudflare 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 Cloudflare API.'
-
- def _validate_credentials(self, credentials: CredentialsConfiguration) ->
None:
- token = credentials.conf('api-token')
- email = credentials.conf('email')
- key = credentials.conf('api-key')
- if token:
- if email or key:
- raise errors.PluginError('{}: dns_cloudflare_email and
dns_cloudflare_api_key are '
- 'not needed when using an API Token'
- .format(credentials.confobj.filename))
- elif email or key:
- if not email:
- raise errors.PluginError('{}: dns_cloudflare_email is required
when using a Global '
- 'API Key. (should be email address
associated with '
- 'Cloudflare
account)'.format(credentials.confobj.filename))
- if not key:
- raise errors.PluginError('{}: dns_cloudflare_api_key is
required when using a '
- 'Global API Key. (see {})'
- .format(credentials.confobj.filename,
ACCOUNT_URL))
- else:
- raise errors.PluginError('{}: Either dns_cloudflare_api_token
(recommended), or '
- 'dns_cloudflare_email and
dns_cloudflare_api_key are required.'
- ' (see
{})'.format(credentials.confobj.filename, ACCOUNT_URL))
-
- def _setup_credentials(self) -> None:
- self.credentials = self._configure_credentials(
- 'credentials',
- 'Cloudflare credentials INI file',
- None,
- self._validate_credentials
- )
-
- def _perform(self, domain: str, validation_name: str, validation: str) ->
None:
- self._get_cloudflare_client().add_txt_record(domain, validation_name,
validation, self.ttl)
-
- def _cleanup(self, domain: str, validation_name: str, validation: str) ->
None:
- self._get_cloudflare_client().del_txt_record(domain, validation_name,
validation)
-
- def _get_cloudflare_client(self) -> "_CloudflareClient":
- if not self.credentials: # pragma: no cover
- raise errors.Error("Plugin has not been prepared.")
- if self.credentials.conf('api-token'):
- return _CloudflareClient(api_token =
self.credentials.conf('api-token'))
- return _CloudflareClient(email = self.credentials.conf('email'),
- api_key = self.credentials.conf('api-key'))
-
-
-class _CloudflareClient:
- """
- Encapsulates all communication with the Cloudflare API.
- """
-
- def __init__(self, email: Optional[str] = None, api_key: Optional[str] =
None,
- api_token: Optional[str] = None) -> None:
- if email:
- # If an email was specified, we're using an email/key combination
and not a token.
- # We can't use named arguments in this case, as it would break
compatibility with
- # the Cloudflare library since version 2.10.1, as the `token`
argument was used for
- # tokens and keys alike and the `key` argument did not exist in
earlier versions.
- self.cf = CloudFlare.CloudFlare(email, api_key)
- else:
- # If no email was specified, we're using just a token. Let's use
the named argument
- # for simplicity, which is compatible with all (current) versions
of the Cloudflare
- # library.
- self.cf = CloudFlare.CloudFlare(token=api_token)
-
- def add_txt_record(self, domain: str, record_name: str, record_content:
str,
- record_ttl: int) -> None:
- """
- Add a TXT record using the supplied information.
-
- :param str domain: The domain to use to look up the Cloudflare zone.
- :param str record_name: The record name (typically beginning with
'_acme-challenge.').
- :param str record_content: The record content (typically the challenge
validation).
- :param int record_ttl: The record TTL (number of seconds that the
record may be cached).
- :raises certbot.errors.PluginError: if an error occurs communicating
with the Cloudflare API
- """
-
- zone_id = self._find_zone_id(domain)
-
- data = {'type': 'TXT',
- 'name': record_name,
- 'content': record_content,
- 'ttl': record_ttl}
-
- try:
- logger.debug('Attempting to add record to zone %s: %s', zone_id,
data)
- self.cf.zones.dns_records.post(zone_id, data=data) # zones |
pylint: disable=no-member
- except CloudFlare.exceptions.CloudFlareAPIError as e:
- code = int(e)
- hint = None
-
- if code == 1009:
- hint = 'Does your API token have "Zone:DNS:Edit" permissions?'
-
- logger.error('Encountered CloudFlareAPIError adding TXT record: %d
%s', e, e)
- raise errors.PluginError('Error communicating with the Cloudflare
API: {0}{1}'
- .format(e, ' ({0})'.format(hint) if hint
else ''))
-
- record_id = self._find_txt_record_id(zone_id, record_name,
record_content)
- logger.debug('Successfully added TXT record with record_id: %s',
record_id)
-
- def del_txt_record(self, domain: str, record_name: str, record_content:
str) -> None:
- """
- Delete a TXT record using the supplied information.
-
- Note that both the record's name and content are used to ensure that
similar records
- created concurrently (e.g., due to concurrent invocations of this
plugin) are not deleted.
-
- Failures are logged, but not raised.
-
- :param str domain: The domain to use to look up the Cloudflare zone.
- :param str record_name: The record name (typically beginning with
'_acme-challenge.').
- :param str record_content: The record content (typically the challenge
validation).
- """
-
- try:
- zone_id = self._find_zone_id(domain)
- except errors.PluginError as e:
- logger.debug('Encountered error finding zone_id during deletion:
%s', e)
- return
-
- if zone_id:
- record_id = self._find_txt_record_id(zone_id, record_name,
record_content)
- if record_id:
- try:
- # zones | pylint: disable=no-member
- self.cf.zones.dns_records.delete(zone_id, record_id)
- logger.debug('Successfully deleted TXT record.')
- except CloudFlare.exceptions.CloudFlareAPIError as e:
- logger.warning('Encountered CloudFlareAPIError deleting
TXT record: %s', e)
- else:
- logger.debug('TXT record not found; no cleanup needed.')
- else:
- logger.debug('Zone not found; no cleanup needed.')
-
- def _find_zone_id(self, domain: str) -> str:
- """
- Find the zone_id for a given domain.
-
- :param str domain: The domain for which to find the zone_id.
- :returns: The zone_id, if found.
- :rtype: str
- :raises certbot.errors.PluginError: if no zone_id is found.
- """
-
- zone_name_guesses = dns_common.base_domain_name_guesses(domain)
- zones: List[Dict[str, Any]] = []
- code = msg = None
-
- for zone_name in zone_name_guesses:
- params = {'name': zone_name,
- 'per_page': 1}
-
- try:
- zones = self.cf.zones.get(params=params) # zones | pylint:
disable=no-member
- except CloudFlare.exceptions.CloudFlareAPIError as e:
- code = int(e)
- msg = str(e)
- hint = None
-
- if code == 6003:
- hint = ('Did you copy your entire API token/key? To use
Cloudflare tokens, '
- 'you\'ll need the python package
cloudflare>=2.3.1.{}'
- .format(' This certbot is running cloudflare ' +
str(CloudFlare.__version__)
- if hasattr(CloudFlare, '__version__') else ''))
- elif code == 9103:
- hint = 'Did you enter the correct email address and Global
key?'
- elif code == 9109:
- hint = 'Did you enter a valid Cloudflare Token?'
-
- if hint:
- raise errors.PluginError('Error determining zone_id: {0}
{1}. Please confirm '
- 'that you have supplied valid Cloudflare API
credentials. ({2})'
-
.format(code, msg, hint))
- else:
- logger.debug('Unrecognised CloudFlareAPIError while
finding zone_id: %d %s. '
- 'Continuing with next zone guess...', e, e)
-
- if zones:
- zone_id: str = zones[0]['id']
- logger.debug('Found zone_id of %s for %s using name %s',
zone_id, domain, zone_name)
- return zone_id
-
- if msg is not None:
- if 'com.cloudflare.api.account.zone.list' in msg:
- raise errors.PluginError('Unable to determine zone_id for {0}
using zone names: '
- '{1}. Please confirm that the domain
name has been '
- 'entered correctly and your
Cloudflare Token has access '
- 'to the domain.'.format(domain,
zone_name_guesses))
- else:
- raise errors.PluginError('Unable to determine zone_id for {0}
using zone names: '
- '{1}. The error from Cloudflare was:
{2} {3}.'
- .format(domain, zone_name_guesses,
code, msg))
- else:
- raise errors.PluginError('Unable to determine zone_id for {0}
using zone names: '
- '{1}. Please confirm that the domain name
has been '
- 'entered correctly and is already
associated with the '
- 'supplied Cloudflare account.'
- .format(domain, zone_name_guesses))
-
- def _find_txt_record_id(self, zone_id: str, record_name: str,
- record_content: str) -> Optional[str]:
- """
- Find the record_id for a TXT record with the given name and content.
-
- :param str zone_id: The zone_id which contains the record.
- :param str record_name: The record name (typically beginning with
'_acme-challenge.').
- :param str record_content: The record content (typically the challenge
validation).
- :returns: The record_id, if found.
- :rtype: str
- """
-
- params = {'type': 'TXT',
- 'name': record_name,
- 'content': record_content,
- 'per_page': 1}
- try:
- # zones | pylint: disable=no-member
- records = self.cf.zones.dns_records.get(zone_id, params=params)
- except CloudFlare.exceptions.CloudFlareAPIError as e:
- logger.debug('Encountered CloudFlareAPIError getting TXT
record_id: %s', e)
- records = []
-
- if records:
- # Cleanup is returning the system to the state we found it. If,
for some reason,
- # there are multiple matching records, we only delete one because
we only added one.
- return cast(str, records[0]['id'])
- logger.debug('Unable to find TXT record.')
- return None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/tests/__init__.py
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/tests/__init__.py
---
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/tests/__init__.py
2025-04-08 00:03:33.000000000 +0200
+++
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/tests/__init__.py
1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-"""certbot-dns-cloudflare tests"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
---
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
2025-04-08 00:03:33.000000000 +0200
+++
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
1970-01-01 01:00:00.000000000 +0100
@@ -1,232 +0,0 @@
-"""Tests for certbot_dns_cloudflare._internal.dns_cloudflare."""
-
-import sys
-import unittest
-from unittest import mock
-
-import CloudFlare
-import pytest
-
-from certbot import errors
-from certbot.compat import os
-from certbot.plugins import dns_test_common
-from certbot.plugins.dns_test_common import DOMAIN
-from certbot.tests import util as test_util
-
-API_ERROR = CloudFlare.exceptions.CloudFlareAPIError(1000, '', '')
-
-API_TOKEN = 'an-api-token'
-
-API_KEY = 'an-api-key'
-EMAIL = '[email protected]'
-
-
-class AuthenticatorTest(test_util.TempDirTestCase,
dns_test_common.BaseAuthenticatorTest):
-
- def setUp(self):
- from certbot_dns_cloudflare._internal.dns_cloudflare import
Authenticator
-
- super().setUp()
-
- path = os.path.join(self.tempdir, 'file.ini')
- dns_test_common.write({"cloudflare_email": EMAIL,
"cloudflare_api_key": API_KEY}, path)
-
- self.config = mock.MagicMock(cloudflare_credentials=path,
- cloudflare_propagation_seconds=0) #
don't wait during tests
-
- self.auth = Authenticator(self.config, "cloudflare")
-
- self.mock_client = mock.MagicMock()
- # _get_cloudflare_client | pylint: disable=protected-access
- # workaround for wont-fix https://github.com/python/mypy/issues/2427
that works with
- # both strict and non-strict mypy
- setattr(self.auth, '_get_cloudflare_client',
mock.MagicMock(return_value=self.mock_client))
-
- @test_util.patch_display_util()
- def test_perform(self, unused_mock_get_utility):
- self.auth.perform([self.achall])
-
- expected = [mock.call.add_txt_record(DOMAIN,
'_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
- assert expected == self.mock_client.mock_calls
-
- def test_cleanup(self):
- # _attempt_cleanup | pylint: disable=protected-access
- self.auth._attempt_cleanup = True
- self.auth.cleanup([self.achall])
-
- expected = [mock.call.del_txt_record(DOMAIN,
'_acme-challenge.'+DOMAIN, mock.ANY)]
- assert expected == self.mock_client.mock_calls
-
- @test_util.patch_display_util()
- def test_api_token(self, unused_mock_get_utility):
- dns_test_common.write({"cloudflare_api_token": API_TOKEN},
- self.config.cloudflare_credentials)
- self.auth.perform([self.achall])
-
- expected = [mock.call.add_txt_record(DOMAIN,
'_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
- assert expected == self.mock_client.mock_calls
-
- def test_no_creds(self):
- dns_test_common.write({}, self.config.cloudflare_credentials)
- with pytest.raises(errors.PluginError):
- self.auth.perform([self.achall])
-
- def test_missing_email_or_key(self):
- dns_test_common.write({"cloudflare_api_key": API_KEY},
self.config.cloudflare_credentials)
- with pytest.raises(errors.PluginError):
- self.auth.perform([self.achall])
-
- dns_test_common.write({"cloudflare_email": EMAIL},
self.config.cloudflare_credentials)
- with pytest.raises(errors.PluginError):
- self.auth.perform([self.achall])
-
- def test_email_or_key_with_token(self):
- dns_test_common.write({"cloudflare_api_token": API_TOKEN,
"cloudflare_email": EMAIL},
- self.config.cloudflare_credentials)
- with pytest.raises(errors.PluginError):
- self.auth.perform([self.achall])
-
- dns_test_common.write({"cloudflare_api_token": API_TOKEN,
"cloudflare_api_key": API_KEY},
- self.config.cloudflare_credentials)
- with pytest.raises(errors.PluginError):
- self.auth.perform([self.achall])
-
- dns_test_common.write({"cloudflare_api_token": API_TOKEN,
"cloudflare_email": EMAIL,
- "cloudflare_api_key": API_KEY},
self.config.cloudflare_credentials)
- with pytest.raises(errors.PluginError):
- self.auth.perform([self.achall])
-
-
-class CloudflareClientTest(unittest.TestCase):
- record_name = "foo"
- record_content = "bar"
- record_ttl = 42
- zone_id = 1
- record_id = 2
-
- def setUp(self):
- from certbot_dns_cloudflare._internal.dns_cloudflare import
_CloudflareClient
-
- self.cloudflare_client = _CloudflareClient(EMAIL, API_KEY)
-
- self.cf = mock.MagicMock()
- self.cloudflare_client.cf = self.cf
-
- def test_add_txt_record(self):
- self.cf.zones.get.return_value = [{'id': self.zone_id}]
-
- self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content,
- self.record_ttl)
-
- self.cf.zones.dns_records.post.assert_called_with(self.zone_id,
data=mock.ANY)
-
- post_data = self.cf.zones.dns_records.post.call_args[1]['data']
-
- assert 'TXT' == post_data['type']
- assert self.record_name == post_data['name']
- assert self.record_content == post_data['content']
- assert self.record_ttl == post_data['ttl']
-
- def test_add_txt_record_error(self):
- self.cf.zones.get.return_value = [{'id': self.zone_id}]
-
- self.cf.zones.dns_records.post.side_effect =
CloudFlare.exceptions.CloudFlareAPIError(1009, '', '')
-
- with pytest.raises(errors.PluginError):
- self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
-
- def test_add_txt_record_error_during_zone_lookup(self):
- self.cf.zones.get.side_effect = API_ERROR
-
- with pytest.raises(errors.PluginError):
- self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
-
- def test_add_txt_record_zone_not_found(self):
- self.cf.zones.get.return_value = []
-
- with pytest.raises(errors.PluginError):
- self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
-
- def test_add_txt_record_bad_creds(self):
- self.cf.zones.get.side_effect =
CloudFlare.exceptions.CloudFlareAPIError(6003, '', '')
- with pytest.raises(errors.PluginError):
- self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
-
- self.cf.zones.get.side_effect =
CloudFlare.exceptions.CloudFlareAPIError(9103, '', '')
- with pytest.raises(errors.PluginError):
- self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
-
- self.cf.zones.get.side_effect =
CloudFlare.exceptions.CloudFlareAPIError(9109, '', '')
- with pytest.raises(errors.PluginError):
- self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
-
- self.cf.zones.get.side_effect =
CloudFlare.exceptions.CloudFlareAPIError(0,
'com.cloudflare.api.account.zone.list', '')
- with pytest.raises(errors.PluginError):
- self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
-
- def test_del_txt_record(self):
- self.cf.zones.get.return_value = [{'id': self.zone_id}]
- self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}]
-
- self.cloudflare_client.del_txt_record(DOMAIN, self.record_name,
self.record_content)
-
- expected = [mock.call.zones.get(params=mock.ANY),
- mock.call.zones.dns_records.get(self.zone_id,
params=mock.ANY),
- mock.call.zones.dns_records.delete(self.zone_id,
self.record_id)]
-
- assert expected == self.cf.mock_calls
-
- get_data = self.cf.zones.dns_records.get.call_args[1]['params']
-
- assert 'TXT' == get_data['type']
- assert self.record_name == get_data['name']
- assert self.record_content == get_data['content']
-
- def test_del_txt_record_error_during_zone_lookup(self):
- self.cf.zones.get.side_effect = API_ERROR
-
- self.cloudflare_client.del_txt_record(DOMAIN, self.record_name,
self.record_content)
-
- def test_del_txt_record_error_during_delete(self):
- self.cf.zones.get.return_value = [{'id': self.zone_id}]
- self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}]
- self.cf.zones.dns_records.delete.side_effect = API_ERROR
-
- self.cloudflare_client.del_txt_record(DOMAIN, self.record_name,
self.record_content)
- expected = [mock.call.zones.get(params=mock.ANY),
- mock.call.zones.dns_records.get(self.zone_id,
params=mock.ANY),
- mock.call.zones.dns_records.delete(self.zone_id,
self.record_id)]
-
- assert expected == self.cf.mock_calls
-
- def test_del_txt_record_error_during_get(self):
- self.cf.zones.get.return_value = [{'id': self.zone_id}]
- self.cf.zones.dns_records.get.side_effect = API_ERROR
-
- self.cloudflare_client.del_txt_record(DOMAIN, self.record_name,
self.record_content)
- expected = [mock.call.zones.get(params=mock.ANY),
- mock.call.zones.dns_records.get(self.zone_id,
params=mock.ANY)]
-
- assert expected == self.cf.mock_calls
-
- def test_del_txt_record_no_record(self):
- self.cf.zones.get.return_value = [{'id': self.zone_id}]
- self.cf.zones.dns_records.get.return_value = []
-
- self.cloudflare_client.del_txt_record(DOMAIN, self.record_name,
self.record_content)
- expected = [mock.call.zones.get(params=mock.ANY),
- mock.call.zones.dns_records.get(self.zone_id,
params=mock.ANY)]
-
- assert expected == self.cf.mock_calls
-
- def test_del_txt_record_no_zone(self):
- self.cf.zones.get.return_value = [{'id': None}]
-
- self.cloudflare_client.del_txt_record(DOMAIN, self.record_name,
self.record_content)
- expected = [mock.call.zones.get(params=mock.ANY)]
-
- assert expected == self.cf.mock_calls
-
-
-if __name__ == "__main__":
- sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/PKG-INFO
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/PKG-INFO
--- old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/PKG-INFO
2025-04-08 00:03:36.000000000 +0200
+++ new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/PKG-INFO
1970-01-01 01:00:00.000000000 +0100
@@ -1,46 +0,0 @@
-Metadata-Version: 2.4
-Name: certbot-dns-cloudflare
-Version: 4.0.0
-Summary: Cloudflare DNS Authenticator plugin for Certbot
-Home-page: https://github.com/certbot/certbot
-Author: Certbot Project
-Author-email: [email protected]
-License: Apache License 2.0
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Environment :: Plugins
-Classifier: Intended Audience :: System Administrators
-Classifier: License :: OSI Approved :: Apache Software License
-Classifier: Operating System :: POSIX :: Linux
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
-Classifier: Programming Language :: Python :: 3.12
-Classifier: Programming Language :: Python :: 3.13
-Classifier: Topic :: Internet :: WWW/HTTP
-Classifier: Topic :: Security
-Classifier: Topic :: System :: Installation/Setup
-Classifier: Topic :: System :: Networking
-Classifier: Topic :: System :: Systems Administration
-Classifier: Topic :: Utilities
-Requires-Python: >=3.9
-License-File: LICENSE.txt
-Requires-Dist: cloudflare<2.20,>=2.19
-Requires-Dist: acme>=4.0.0
-Requires-Dist: certbot>=4.0.0
-Provides-Extra: docs
-Requires-Dist: Sphinx>=1.0; extra == "docs"
-Requires-Dist: sphinx_rtd_theme; extra == "docs"
-Provides-Extra: test
-Requires-Dist: pytest; extra == "test"
-Dynamic: author
-Dynamic: author-email
-Dynamic: classifier
-Dynamic: home-page
-Dynamic: license
-Dynamic: license-file
-Dynamic: provides-extra
-Dynamic: requires-dist
-Dynamic: requires-python
-Dynamic: summary
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/SOURCES.txt
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/SOURCES.txt
---
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/SOURCES.txt
2025-04-08 00:03:36.000000000 +0200
+++
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/SOURCES.txt
1970-01-01 01:00:00.000000000 +0100
@@ -1,22 +0,0 @@
-LICENSE.txt
-MANIFEST.in
-README.rst
-setup.py
-certbot_dns_cloudflare/__init__.py
-certbot_dns_cloudflare/py.typed
-certbot_dns_cloudflare.egg-info/PKG-INFO
-certbot_dns_cloudflare.egg-info/SOURCES.txt
-certbot_dns_cloudflare.egg-info/dependency_links.txt
-certbot_dns_cloudflare.egg-info/entry_points.txt
-certbot_dns_cloudflare.egg-info/requires.txt
-certbot_dns_cloudflare.egg-info/top_level.txt
-certbot_dns_cloudflare/_internal/__init__.py
-certbot_dns_cloudflare/_internal/dns_cloudflare.py
-certbot_dns_cloudflare/_internal/tests/__init__.py
-certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
-docs/.gitignore
-docs/Makefile
-docs/api.rst
-docs/conf.py
-docs/index.rst
-docs/make.bat
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/dependency_links.txt
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/dependency_links.txt
---
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/dependency_links.txt
2025-04-08 00:03:36.000000000 +0200
+++
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/dependency_links.txt
1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/entry_points.txt
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/entry_points.txt
---
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/entry_points.txt
2025-04-08 00:03:36.000000000 +0200
+++
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/entry_points.txt
1970-01-01 01:00:00.000000000 +0100
@@ -1,2 +0,0 @@
-[certbot.plugins]
-dns-cloudflare = certbot_dns_cloudflare._internal.dns_cloudflare:Authenticator
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/requires.txt
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/requires.txt
---
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/requires.txt
2025-04-08 00:03:36.000000000 +0200
+++
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/requires.txt
1970-01-01 01:00:00.000000000 +0100
@@ -1,10 +0,0 @@
-cloudflare<2.20,>=2.19
-acme>=4.0.0
-certbot>=4.0.0
-
-[docs]
-Sphinx>=1.0
-sphinx_rtd_theme
-
-[test]
-pytest
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/top_level.txt
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/top_level.txt
---
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/top_level.txt
2025-04-08 00:03:36.000000000 +0200
+++
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/top_level.txt
1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-certbot_dns_cloudflare
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/certbot_dns_cloudflare-4.0.0/setup.py
new/certbot_dns_cloudflare-4.1.1/setup.py
--- old/certbot_dns_cloudflare-4.0.0/setup.py 2025-04-08 00:03:33.000000000
+0200
+++ new/certbot_dns_cloudflare-4.1.1/setup.py 2025-06-12 20:08:35.000000000
+0200
@@ -4,7 +4,7 @@
from setuptools import find_packages
from setuptools import setup
-version = '4.0.0'
+version = '4.1.1'
install_requires = [
# for now, do not upgrade to cloudflare>=2.20 to avoid deprecation
warnings and the breaking
@@ -40,7 +40,7 @@
author="Certbot Project",
author_email='[email protected]',
license='Apache License 2.0',
- python_requires='>=3.9',
+ python_requires='>=3.9.2',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Plugins',
@@ -62,7 +62,8 @@
'Topic :: Utilities',
],
- packages=find_packages(),
+ packages=find_packages(where='src'),
+ package_dir={'': 'src'},
include_package_data=True,
install_requires=install_requires,
extras_require={
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/__init__.py
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/__init__.py
--- old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/__init__.py
1970-01-01 01:00:00.000000000 +0100
+++ new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/__init__.py
2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1,124 @@
+"""
+The `~certbot_dns_cloudflare.dns_cloudflare` plugin automates the process of
+completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
+subsequently removing, TXT records using the Cloudflare API.
+
+.. note::
+ The plugin is not installed by default. It can be installed by heading to
+ `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_,
choosing your system and
+ selecting the Wildcard tab.
+
+Named Arguments
+---------------
+
+======================================== =====================================
+``--dns-cloudflare-credentials`` Cloudflare credentials_ INI file.
+ (Required)
+``--dns-cloudflare-propagation-seconds`` The number of seconds to wait for DNS
+ to propagate before asking the ACME
+ server to verify the DNS record.
+ (Default: 10)
+======================================== =====================================
+
+
+Credentials
+-----------
+
+Use of this plugin requires a configuration file containing Cloudflare API
+credentials, obtained from your
+`Cloudflare dashboard
<https://dash.cloudflare.com/?to=/:account/profile/api-tokens>`_.
+
+Previously, Cloudflare's "Global API Key" was used for authentication, however
+this key can access the entire Cloudflare API for all domains in your account,
+meaning it could cause a lot of damage if leaked.
+
+Cloudflare's newer API Tokens can be restricted to specific domains and
+operations, and are therefore now the recommended authentication option.
+
+The Token needed by Certbot requires ``Zone:DNS:Edit`` permissions for only the
+zones you need certificates for.
+
+Using Cloudflare Tokens also requires at least version 2.3.1 of the
``cloudflare``
+Python module. If the version that automatically installed with this plugin is
+older than that, and you can't upgrade it on your system, you'll have to stick
to
+the Global key.
+
+.. code-block:: ini
+ :name: certbot_cloudflare_token.ini
+ :caption: Example credentials file using restricted API Token (recommended):
+
+ # Cloudflare API token used by Certbot
+ dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567
+
+.. code-block:: ini
+ :name: certbot_cloudflare_key.ini
+ :caption: Example credentials file using Global API Key (not recommended):
+
+ # Cloudflare API credentials used by Certbot
+ dns_cloudflare_email = [email protected]
+ dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234
+
+The path to this file can be provided interactively or using the
+``--dns-cloudflare-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
+ Cloudflare 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).
+
+.. note::
+ Please note that the ``cloudflare`` Python module used by the plugin has
+ additional methods of providing credentials to the module, e.g. environment
+ variables or the ``cloudflare.cfg`` configuration file. These methods are
not
+ supported by Certbot. If any of those additional methods of providing
+ credentials is being used, they must provide the same credentials (i.e.,
+ email and API key *or* an API token) as the credentials file provided to
+ Certbot. If there is a discrepancy, the ``cloudflare`` Python module will
+ raise an error. Also note that the credentials provided to Certbot will
take
+ precedence over any other method of providing credentials to the
``cloudflare``
+ Python module.
+
+
+Examples
+--------
+
+.. code-block:: bash
+ :caption: To acquire a certificate for ``example.com``
+
+ certbot certonly \\
+ --dns-cloudflare \\
+ --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\
+ -d example.com
+
+.. code-block:: bash
+ :caption: To acquire a single certificate for both ``example.com`` and
+ ``www.example.com``
+
+ certbot certonly \\
+ --dns-cloudflare \\
+ --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.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-cloudflare \\
+ --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\
+ --dns-cloudflare-propagation-seconds 60 \\
+ -d example.com
+
+"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/__init__.py
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/__init__.py
---
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/__init__.py
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/__init__.py
2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1 @@
+"""Internal implementation of `~certbot_dns_cloudflare.dns_cloudflare`
plugin."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py
---
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py
2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1,273 @@
+"""DNS Authenticator for Cloudflare."""
+import logging
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import cast
+
+import CloudFlare
+
+from certbot import errors
+from certbot.plugins import dns_common
+from certbot.plugins.dns_common import CredentialsConfiguration
+
+logger = logging.getLogger(__name__)
+
+ACCOUNT_URL = 'https://dash.cloudflare.com/?to=/:account/profile/api-tokens'
+
+
+class Authenticator(dns_common.DNSAuthenticator):
+ """DNS Authenticator for Cloudflare
+
+ This Authenticator uses the Cloudflare API to fulfill a dns-01 challenge.
+ """
+
+ description = ('Obtain certificates using a DNS TXT record (if you are
using Cloudflare for '
+ 'DNS).')
+ ttl = 120
+
+ 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 = 10) -> None:
+ super().add_parser_arguments(add, default_propagation_seconds)
+ add('credentials', help='Cloudflare 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 Cloudflare API.'
+
+ def _validate_credentials(self, credentials: CredentialsConfiguration) ->
None:
+ token = credentials.conf('api-token')
+ email = credentials.conf('email')
+ key = credentials.conf('api-key')
+ if token:
+ if email or key:
+ raise errors.PluginError('{}: dns_cloudflare_email and
dns_cloudflare_api_key are '
+ 'not needed when using an API Token'
+ .format(credentials.confobj.filename))
+ elif email or key:
+ if not email:
+ raise errors.PluginError('{}: dns_cloudflare_email is required
when using a Global '
+ 'API Key. (should be email address
associated with '
+ 'Cloudflare
account)'.format(credentials.confobj.filename))
+ if not key:
+ raise errors.PluginError('{}: dns_cloudflare_api_key is
required when using a '
+ 'Global API Key. (see {})'
+ .format(credentials.confobj.filename,
ACCOUNT_URL))
+ else:
+ raise errors.PluginError('{}: Either dns_cloudflare_api_token
(recommended), or '
+ 'dns_cloudflare_email and
dns_cloudflare_api_key are required.'
+ ' (see
{})'.format(credentials.confobj.filename, ACCOUNT_URL))
+
+ def _setup_credentials(self) -> None:
+ self.credentials = self._configure_credentials(
+ 'credentials',
+ 'Cloudflare credentials INI file',
+ None,
+ self._validate_credentials
+ )
+
+ def _perform(self, domain: str, validation_name: str, validation: str) ->
None:
+ self._get_cloudflare_client().add_txt_record(domain, validation_name,
validation, self.ttl)
+
+ def _cleanup(self, domain: str, validation_name: str, validation: str) ->
None:
+ self._get_cloudflare_client().del_txt_record(domain, validation_name,
validation)
+
+ def _get_cloudflare_client(self) -> "_CloudflareClient":
+ if not self.credentials: # pragma: no cover
+ raise errors.Error("Plugin has not been prepared.")
+ if self.credentials.conf('api-token'):
+ return _CloudflareClient(api_token =
self.credentials.conf('api-token'))
+ return _CloudflareClient(email = self.credentials.conf('email'),
+ api_key = self.credentials.conf('api-key'))
+
+
+class _CloudflareClient:
+ """
+ Encapsulates all communication with the Cloudflare API.
+ """
+
+ def __init__(self, email: Optional[str] = None, api_key: Optional[str] =
None,
+ api_token: Optional[str] = None) -> None:
+ if email:
+ # If an email was specified, we're using an email/key combination
and not a token.
+ # We can't use named arguments in this case, as it would break
compatibility with
+ # the Cloudflare library since version 2.10.1, as the `token`
argument was used for
+ # tokens and keys alike and the `key` argument did not exist in
earlier versions.
+ self.cf = CloudFlare.CloudFlare(email, api_key)
+ else:
+ # If no email was specified, we're using just a token. Let's use
the named argument
+ # for simplicity, which is compatible with all (current) versions
of the Cloudflare
+ # library.
+ self.cf = CloudFlare.CloudFlare(token=api_token)
+
+ def add_txt_record(self, domain: str, record_name: str, record_content:
str,
+ record_ttl: int) -> None:
+ """
+ Add a TXT record using the supplied information.
+
+ :param str domain: The domain to use to look up the Cloudflare zone.
+ :param str record_name: The record name (typically beginning with
'_acme-challenge.').
+ :param str record_content: The record content (typically the challenge
validation).
+ :param int record_ttl: The record TTL (number of seconds that the
record may be cached).
+ :raises certbot.errors.PluginError: if an error occurs communicating
with the Cloudflare API
+ """
+
+ zone_id = self._find_zone_id(domain)
+
+ data = {'type': 'TXT',
+ 'name': record_name,
+ 'content': record_content,
+ 'ttl': record_ttl}
+
+ try:
+ logger.debug('Attempting to add record to zone %s: %s', zone_id,
data)
+ self.cf.zones.dns_records.post(zone_id, data=data) # zones |
pylint: disable=no-member
+ except CloudFlare.exceptions.CloudFlareAPIError as e:
+ code = int(e)
+ hint = None
+
+ if code == 1009:
+ hint = 'Does your API token have "Zone:DNS:Edit" permissions?'
+
+ logger.error('Encountered CloudFlareAPIError adding TXT record: %d
%s', e, e)
+ raise errors.PluginError('Error communicating with the Cloudflare
API: {0}{1}'
+ .format(e, ' ({0})'.format(hint) if hint
else ''))
+
+ record_id = self._find_txt_record_id(zone_id, record_name,
record_content)
+ logger.debug('Successfully added TXT record with record_id: %s',
record_id)
+
+ def del_txt_record(self, domain: str, record_name: str, record_content:
str) -> None:
+ """
+ Delete a TXT record using the supplied information.
+
+ Note that both the record's name and content are used to ensure that
similar records
+ created concurrently (e.g., due to concurrent invocations of this
plugin) are not deleted.
+
+ Failures are logged, but not raised.
+
+ :param str domain: The domain to use to look up the Cloudflare zone.
+ :param str record_name: The record name (typically beginning with
'_acme-challenge.').
+ :param str record_content: The record content (typically the challenge
validation).
+ """
+
+ try:
+ zone_id = self._find_zone_id(domain)
+ except errors.PluginError as e:
+ logger.debug('Encountered error finding zone_id during deletion:
%s', e)
+ return
+
+ if zone_id:
+ record_id = self._find_txt_record_id(zone_id, record_name,
record_content)
+ if record_id:
+ try:
+ # zones | pylint: disable=no-member
+ self.cf.zones.dns_records.delete(zone_id, record_id)
+ logger.debug('Successfully deleted TXT record.')
+ except CloudFlare.exceptions.CloudFlareAPIError as e:
+ logger.warning('Encountered CloudFlareAPIError deleting
TXT record: %s', e)
+ else:
+ logger.debug('TXT record not found; no cleanup needed.')
+ else:
+ logger.debug('Zone not found; no cleanup needed.')
+
+ def _find_zone_id(self, domain: str) -> str:
+ """
+ Find the zone_id for a given domain.
+
+ :param str domain: The domain for which to find the zone_id.
+ :returns: The zone_id, if found.
+ :rtype: str
+ :raises certbot.errors.PluginError: if no zone_id is found.
+ """
+
+ zone_name_guesses = dns_common.base_domain_name_guesses(domain)
+ zones: List[Dict[str, Any]] = []
+ code = msg = None
+
+ for zone_name in zone_name_guesses:
+ params = {'name': zone_name,
+ 'per_page': 1}
+
+ try:
+ zones = self.cf.zones.get(params=params) # zones | pylint:
disable=no-member
+ except CloudFlare.exceptions.CloudFlareAPIError as e:
+ code = int(e)
+ msg = str(e)
+ hint = None
+
+ if code == 6003:
+ hint = ('Did you copy your entire API token/key? To use
Cloudflare tokens, '
+ 'you\'ll need the python package
cloudflare>=2.3.1.{}'
+ .format(' This certbot is running cloudflare ' +
str(CloudFlare.__version__)
+ if hasattr(CloudFlare, '__version__') else ''))
+ elif code == 9103:
+ hint = 'Did you enter the correct email address and Global
key?'
+ elif code == 9109:
+ hint = 'Did you enter a valid Cloudflare Token?'
+
+ if hint:
+ raise errors.PluginError('Error determining zone_id: {0}
{1}. Please confirm '
+ 'that you have supplied valid Cloudflare API
credentials. ({2})'
+
.format(code, msg, hint))
+ else:
+ logger.debug('Unrecognised CloudFlareAPIError while
finding zone_id: %d %s. '
+ 'Continuing with next zone guess...', e, e)
+
+ if zones:
+ zone_id: str = zones[0]['id']
+ logger.debug('Found zone_id of %s for %s using name %s',
zone_id, domain, zone_name)
+ return zone_id
+
+ if msg is not None:
+ if 'com.cloudflare.api.account.zone.list' in msg:
+ raise errors.PluginError('Unable to determine zone_id for {0}
using zone names: '
+ '{1}. Please confirm that the domain
name has been '
+ 'entered correctly and your
Cloudflare Token has access '
+ 'to the domain.'.format(domain,
zone_name_guesses))
+ else:
+ raise errors.PluginError('Unable to determine zone_id for {0}
using zone names: '
+ '{1}. The error from Cloudflare was:
{2} {3}.'
+ .format(domain, zone_name_guesses,
code, msg))
+ else:
+ raise errors.PluginError('Unable to determine zone_id for {0}
using zone names: '
+ '{1}. Please confirm that the domain name
has been '
+ 'entered correctly and is already
associated with the '
+ 'supplied Cloudflare account.'
+ .format(domain, zone_name_guesses))
+
+ def _find_txt_record_id(self, zone_id: str, record_name: str,
+ record_content: str) -> Optional[str]:
+ """
+ Find the record_id for a TXT record with the given name and content.
+
+ :param str zone_id: The zone_id which contains the record.
+ :param str record_name: The record name (typically beginning with
'_acme-challenge.').
+ :param str record_content: The record content (typically the challenge
validation).
+ :returns: The record_id, if found.
+ :rtype: str
+ """
+
+ params = {'type': 'TXT',
+ 'name': record_name,
+ 'content': record_content,
+ 'per_page': 1}
+ try:
+ # zones | pylint: disable=no-member
+ records = self.cf.zones.dns_records.get(zone_id, params=params)
+ except CloudFlare.exceptions.CloudFlareAPIError as e:
+ logger.debug('Encountered CloudFlareAPIError getting TXT
record_id: %s', e)
+ records = []
+
+ if records:
+ # Cleanup is returning the system to the state we found it. If,
for some reason,
+ # there are multiple matching records, we only delete one because
we only added one.
+ return cast(str, records[0]['id'])
+ logger.debug('Unable to find TXT record.')
+ return None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/tests/__init__.py
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/tests/__init__.py
---
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/tests/__init__.py
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/tests/__init__.py
2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1 @@
+"""certbot-dns-cloudflare tests"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
---
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1,232 @@
+"""Tests for certbot_dns_cloudflare._internal.dns_cloudflare."""
+
+import sys
+import unittest
+from unittest import mock
+
+import CloudFlare
+import pytest
+
+from certbot import errors
+from certbot.compat import os
+from certbot.plugins import dns_test_common
+from certbot.plugins.dns_test_common import DOMAIN
+from certbot.tests import util as test_util
+
+API_ERROR = CloudFlare.exceptions.CloudFlareAPIError(1000, '', '')
+
+API_TOKEN = 'an-api-token'
+
+API_KEY = 'an-api-key'
+EMAIL = '[email protected]'
+
+
+class AuthenticatorTest(test_util.TempDirTestCase,
dns_test_common.BaseAuthenticatorTest):
+
+ def setUp(self):
+ from certbot_dns_cloudflare._internal.dns_cloudflare import
Authenticator
+
+ super().setUp()
+
+ path = os.path.join(self.tempdir, 'file.ini')
+ dns_test_common.write({"cloudflare_email": EMAIL,
"cloudflare_api_key": API_KEY}, path)
+
+ self.config = mock.MagicMock(cloudflare_credentials=path,
+ cloudflare_propagation_seconds=0) #
don't wait during tests
+
+ self.auth = Authenticator(self.config, "cloudflare")
+
+ self.mock_client = mock.MagicMock()
+ # _get_cloudflare_client | pylint: disable=protected-access
+ # workaround for wont-fix https://github.com/python/mypy/issues/2427
that works with
+ # both strict and non-strict mypy
+ setattr(self.auth, '_get_cloudflare_client',
mock.MagicMock(return_value=self.mock_client))
+
+ @test_util.patch_display_util()
+ def test_perform(self, unused_mock_get_utility):
+ self.auth.perform([self.achall])
+
+ expected = [mock.call.add_txt_record(DOMAIN,
'_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
+ assert expected == self.mock_client.mock_calls
+
+ def test_cleanup(self):
+ # _attempt_cleanup | pylint: disable=protected-access
+ self.auth._attempt_cleanup = True
+ self.auth.cleanup([self.achall])
+
+ expected = [mock.call.del_txt_record(DOMAIN,
'_acme-challenge.'+DOMAIN, mock.ANY)]
+ assert expected == self.mock_client.mock_calls
+
+ @test_util.patch_display_util()
+ def test_api_token(self, unused_mock_get_utility):
+ dns_test_common.write({"cloudflare_api_token": API_TOKEN},
+ self.config.cloudflare_credentials)
+ self.auth.perform([self.achall])
+
+ expected = [mock.call.add_txt_record(DOMAIN,
'_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
+ assert expected == self.mock_client.mock_calls
+
+ def test_no_creds(self):
+ dns_test_common.write({}, self.config.cloudflare_credentials)
+ with pytest.raises(errors.PluginError):
+ self.auth.perform([self.achall])
+
+ def test_missing_email_or_key(self):
+ dns_test_common.write({"cloudflare_api_key": API_KEY},
self.config.cloudflare_credentials)
+ with pytest.raises(errors.PluginError):
+ self.auth.perform([self.achall])
+
+ dns_test_common.write({"cloudflare_email": EMAIL},
self.config.cloudflare_credentials)
+ with pytest.raises(errors.PluginError):
+ self.auth.perform([self.achall])
+
+ def test_email_or_key_with_token(self):
+ dns_test_common.write({"cloudflare_api_token": API_TOKEN,
"cloudflare_email": EMAIL},
+ self.config.cloudflare_credentials)
+ with pytest.raises(errors.PluginError):
+ self.auth.perform([self.achall])
+
+ dns_test_common.write({"cloudflare_api_token": API_TOKEN,
"cloudflare_api_key": API_KEY},
+ self.config.cloudflare_credentials)
+ with pytest.raises(errors.PluginError):
+ self.auth.perform([self.achall])
+
+ dns_test_common.write({"cloudflare_api_token": API_TOKEN,
"cloudflare_email": EMAIL,
+ "cloudflare_api_key": API_KEY},
self.config.cloudflare_credentials)
+ with pytest.raises(errors.PluginError):
+ self.auth.perform([self.achall])
+
+
+class CloudflareClientTest(unittest.TestCase):
+ record_name = "foo"
+ record_content = "bar"
+ record_ttl = 42
+ zone_id = 1
+ record_id = 2
+
+ def setUp(self):
+ from certbot_dns_cloudflare._internal.dns_cloudflare import
_CloudflareClient
+
+ self.cloudflare_client = _CloudflareClient(EMAIL, API_KEY)
+
+ self.cf = mock.MagicMock()
+ self.cloudflare_client.cf = self.cf
+
+ def test_add_txt_record(self):
+ self.cf.zones.get.return_value = [{'id': self.zone_id}]
+
+ self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content,
+ self.record_ttl)
+
+ self.cf.zones.dns_records.post.assert_called_with(self.zone_id,
data=mock.ANY)
+
+ post_data = self.cf.zones.dns_records.post.call_args[1]['data']
+
+ assert 'TXT' == post_data['type']
+ assert self.record_name == post_data['name']
+ assert self.record_content == post_data['content']
+ assert self.record_ttl == post_data['ttl']
+
+ def test_add_txt_record_error(self):
+ self.cf.zones.get.return_value = [{'id': self.zone_id}]
+
+ self.cf.zones.dns_records.post.side_effect =
CloudFlare.exceptions.CloudFlareAPIError(1009, '', '')
+
+ with pytest.raises(errors.PluginError):
+ self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
+
+ def test_add_txt_record_error_during_zone_lookup(self):
+ self.cf.zones.get.side_effect = API_ERROR
+
+ with pytest.raises(errors.PluginError):
+ self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
+
+ def test_add_txt_record_zone_not_found(self):
+ self.cf.zones.get.return_value = []
+
+ with pytest.raises(errors.PluginError):
+ self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
+
+ def test_add_txt_record_bad_creds(self):
+ self.cf.zones.get.side_effect =
CloudFlare.exceptions.CloudFlareAPIError(6003, '', '')
+ with pytest.raises(errors.PluginError):
+ self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
+
+ self.cf.zones.get.side_effect =
CloudFlare.exceptions.CloudFlareAPIError(9103, '', '')
+ with pytest.raises(errors.PluginError):
+ self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
+
+ self.cf.zones.get.side_effect =
CloudFlare.exceptions.CloudFlareAPIError(9109, '', '')
+ with pytest.raises(errors.PluginError):
+ self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
+
+ self.cf.zones.get.side_effect =
CloudFlare.exceptions.CloudFlareAPIError(0,
'com.cloudflare.api.account.zone.list', '')
+ with pytest.raises(errors.PluginError):
+ self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
+
+ def test_del_txt_record(self):
+ self.cf.zones.get.return_value = [{'id': self.zone_id}]
+ self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}]
+
+ self.cloudflare_client.del_txt_record(DOMAIN, self.record_name,
self.record_content)
+
+ expected = [mock.call.zones.get(params=mock.ANY),
+ mock.call.zones.dns_records.get(self.zone_id,
params=mock.ANY),
+ mock.call.zones.dns_records.delete(self.zone_id,
self.record_id)]
+
+ assert expected == self.cf.mock_calls
+
+ get_data = self.cf.zones.dns_records.get.call_args[1]['params']
+
+ assert 'TXT' == get_data['type']
+ assert self.record_name == get_data['name']
+ assert self.record_content == get_data['content']
+
+ def test_del_txt_record_error_during_zone_lookup(self):
+ self.cf.zones.get.side_effect = API_ERROR
+
+ self.cloudflare_client.del_txt_record(DOMAIN, self.record_name,
self.record_content)
+
+ def test_del_txt_record_error_during_delete(self):
+ self.cf.zones.get.return_value = [{'id': self.zone_id}]
+ self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}]
+ self.cf.zones.dns_records.delete.side_effect = API_ERROR
+
+ self.cloudflare_client.del_txt_record(DOMAIN, self.record_name,
self.record_content)
+ expected = [mock.call.zones.get(params=mock.ANY),
+ mock.call.zones.dns_records.get(self.zone_id,
params=mock.ANY),
+ mock.call.zones.dns_records.delete(self.zone_id,
self.record_id)]
+
+ assert expected == self.cf.mock_calls
+
+ def test_del_txt_record_error_during_get(self):
+ self.cf.zones.get.return_value = [{'id': self.zone_id}]
+ self.cf.zones.dns_records.get.side_effect = API_ERROR
+
+ self.cloudflare_client.del_txt_record(DOMAIN, self.record_name,
self.record_content)
+ expected = [mock.call.zones.get(params=mock.ANY),
+ mock.call.zones.dns_records.get(self.zone_id,
params=mock.ANY)]
+
+ assert expected == self.cf.mock_calls
+
+ def test_del_txt_record_no_record(self):
+ self.cf.zones.get.return_value = [{'id': self.zone_id}]
+ self.cf.zones.dns_records.get.return_value = []
+
+ self.cloudflare_client.del_txt_record(DOMAIN, self.record_name,
self.record_content)
+ expected = [mock.call.zones.get(params=mock.ANY),
+ mock.call.zones.dns_records.get(self.zone_id,
params=mock.ANY)]
+
+ assert expected == self.cf.mock_calls
+
+ def test_del_txt_record_no_zone(self):
+ self.cf.zones.get.return_value = [{'id': None}]
+
+ self.cloudflare_client.del_txt_record(DOMAIN, self.record_name,
self.record_content)
+ expected = [mock.call.zones.get(params=mock.ANY)]
+
+ assert expected == self.cf.mock_calls
+
+
+if __name__ == "__main__":
+ sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/PKG-INFO
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/PKG-INFO
---
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/PKG-INFO
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/PKG-INFO
2025-06-12 20:08:38.000000000 +0200
@@ -0,0 +1,46 @@
+Metadata-Version: 2.4
+Name: certbot-dns-cloudflare
+Version: 4.1.1
+Summary: Cloudflare DNS Authenticator plugin for Certbot
+Home-page: https://github.com/certbot/certbot
+Author: Certbot Project
+Author-email: [email protected]
+License: Apache License 2.0
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Plugins
+Classifier: Intended Audience :: System Administrators
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Topic :: Internet :: WWW/HTTP
+Classifier: Topic :: Security
+Classifier: Topic :: System :: Installation/Setup
+Classifier: Topic :: System :: Networking
+Classifier: Topic :: System :: Systems Administration
+Classifier: Topic :: Utilities
+Requires-Python: >=3.9.2
+License-File: LICENSE.txt
+Requires-Dist: cloudflare<2.20,>=2.19
+Requires-Dist: acme>=4.1.1
+Requires-Dist: certbot>=4.1.1
+Provides-Extra: docs
+Requires-Dist: Sphinx>=1.0; extra == "docs"
+Requires-Dist: sphinx_rtd_theme; extra == "docs"
+Provides-Extra: test
+Requires-Dist: pytest; extra == "test"
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: home-page
+Dynamic: license
+Dynamic: license-file
+Dynamic: provides-extra
+Dynamic: requires-dist
+Dynamic: requires-python
+Dynamic: summary
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/SOURCES.txt
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/SOURCES.txt
---
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/SOURCES.txt
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/SOURCES.txt
2025-06-12 20:08:38.000000000 +0200
@@ -0,0 +1,22 @@
+LICENSE.txt
+MANIFEST.in
+README.rst
+setup.py
+docs/.gitignore
+docs/Makefile
+docs/api.rst
+docs/conf.py
+docs/index.rst
+docs/make.bat
+src/certbot_dns_cloudflare/__init__.py
+src/certbot_dns_cloudflare/py.typed
+src/certbot_dns_cloudflare.egg-info/PKG-INFO
+src/certbot_dns_cloudflare.egg-info/SOURCES.txt
+src/certbot_dns_cloudflare.egg-info/dependency_links.txt
+src/certbot_dns_cloudflare.egg-info/entry_points.txt
+src/certbot_dns_cloudflare.egg-info/requires.txt
+src/certbot_dns_cloudflare.egg-info/top_level.txt
+src/certbot_dns_cloudflare/_internal/__init__.py
+src/certbot_dns_cloudflare/_internal/dns_cloudflare.py
+src/certbot_dns_cloudflare/_internal/tests/__init__.py
+src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/dependency_links.txt
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/dependency_links.txt
---
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/dependency_links.txt
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/dependency_links.txt
2025-06-12 20:08:38.000000000 +0200
@@ -0,0 +1 @@
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/entry_points.txt
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/entry_points.txt
---
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/entry_points.txt
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/entry_points.txt
2025-06-12 20:08:38.000000000 +0200
@@ -0,0 +1,2 @@
+[certbot.plugins]
+dns-cloudflare = certbot_dns_cloudflare._internal.dns_cloudflare:Authenticator
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/requires.txt
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/requires.txt
---
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/requires.txt
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/requires.txt
2025-06-12 20:08:38.000000000 +0200
@@ -0,0 +1,10 @@
+cloudflare<2.20,>=2.19
+acme>=4.1.1
+certbot>=4.1.1
+
+[docs]
+Sphinx>=1.0
+sphinx_rtd_theme
+
+[test]
+pytest
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/top_level.txt
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/top_level.txt
---
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/top_level.txt
1970-01-01 01:00:00.000000000 +0100
+++
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/top_level.txt
2025-06-12 20:08:38.000000000 +0200
@@ -0,0 +1 @@
+certbot_dns_cloudflare