Package: release.debian.org
Severity: normal
X-Debbugs-Cc: python-certbot-ng...@packages.debian.org, hlieber...@debian.org
Control: affects -1 + src:python-certbot-nginx
User: release.debian....@packages.debian.org
Usertags: unblock
User: hlieber...@debian.org
Usertags: trixie-certbot

Please unblock package python-certbot-nginx

[ Reason ]

The certbot 2.x series is end of life and will not receive further updates or
backports of changes.
(https://github.com/certbot/certbot/wiki/Architectural-Decision-Records-2025#-update-to-certbots-version-policy-and-end-of-life-support-on-previous-major-versions)
By far and away, the primary purpose of certbot is to receive certificates from
Let's Encrypt, and the Let's Encrypt team are planning API changes in 2025 which
will break the issuance of TLS certificates for people using the Certbot 2.x
series.

[ Impact ]

If the unblock is not granted, certbot will suddenly stop working at
some point in the future and users' TLS certificates will expire. Because
certbot tends to be used as a set-it-and-forget-it system, and Let's Encrypt has
recently disabled their email notifications, users' websites and applications
may suddenly be unavailable to users and/or vulnerable to MitM.

[ Tests ]

Certbot's two primary plugins (python-certbot-apache, python-certbot-nginx) and
the main utility (python-certbot) have a test harness which exercises the entire
process of getting a certificate against a test environment. This provides very
high confidence that those packages are still working, and that the libraries
which they depend on (python-josepy, python-acme) are in good health. These
tests pass cleanly on ci.d.n for all three invocations.

The dns plugin packages (python-certbot-dns-*) are substantially less
complicated than the other certbot packages and primarily handle communication
with various companies' API layers. Those are unlikely to have broken because of
the changes to certbot's internals; the primary way in which those packages
break are due to API changes on the providers' ends.

[ Risks ]

Upgrading the packages across major versions comes with risks, certainly, but
there is little in the way of alternative. The changes are too complex for me to
be willing to attempt to backport, and in a security critical application, I am
even more reticent than I normally would be. I recognize the late application
introduces even more risk --- and rightfully, I'm sure no small amount of
annoyance --- but it is where we've ended up.

[ Checklist ]
  [X] all changes are documented in the d/changelog
  [X] I reviewed all changes and I approve them
  [X] attach debdiff against the package in testing

[ Other info ]

This is one of a series of identical unblock requests with only
package names and debdiffs differing.

unblock python-certbot-nginx/4.0.0-2
diff -Nru python-certbot-nginx-2.9.0/certbot_nginx/_internal/configurator.py 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/configurator.py
--- python-certbot-nginx-2.9.0/certbot_nginx/_internal/configurator.py  
2024-02-08 14:45:17.000000000 -0500
+++ python-certbot-nginx-4.0.0/certbot_nginx/_internal/configurator.py  
2025-04-07 18:03:33.000000000 -0400
@@ -3,10 +3,10 @@
 import atexit
 from contextlib import ExitStack
 import logging
+import importlib.resources
 import re
 import socket
 import subprocess
-import sys
 import tempfile
 import time
 from typing import Any
@@ -21,8 +21,10 @@
 from typing import Tuple
 from typing import Type
 from typing import Union
+from typing import cast
 
-import OpenSSL
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
 
 from acme import challenges
 from acme import crypto_util as acme_crypto_util
@@ -40,11 +42,6 @@
 from certbot_nginx._internal import obj
 from certbot_nginx._internal import parser
 
-if sys.version_info >= (3, 9):  # pragma: no cover
-    import importlib.resources as importlib_resources
-else:  # pragma: no cover
-    import importlib_resources
-
 NAME_RANK = 0
 START_WILDCARD_RANK = 1
 END_WILDCARD_RANK = 2
@@ -171,10 +168,10 @@
 
         file_manager = ExitStack()
         atexit.register(file_manager.close)
-        ref = (importlib_resources.files("certbot_nginx").joinpath("_internal")
+        ref = (importlib.resources.files("certbot_nginx").joinpath("_internal")
                .joinpath("tls_configs").joinpath(config_filename))
 
-        return 
str(file_manager.enter_context(importlib_resources.as_file(ref)))
+        return 
str(file_manager.enter_context(importlib.resources.as_file(ref)))
 
     @property
     def mod_ssl_conf(self) -> str:
@@ -375,13 +372,14 @@
 
         return vhosts
 
-    def ipv6_info(self, port: str) -> Tuple[bool, bool]:
+    def ipv6_info(self, host: str, port: str) -> Tuple[bool, bool]:
         """Returns tuple of booleans (ipv6_active, ipv6only_present)
         ipv6_active is true if any server block listens ipv6 address in any 
port
 
         ipv6only_present is true if ipv6only=on option exists in any server
         block ipv6 listen directive for the specified port.
 
+        :param str host: Host to check ipv6only=on directive for
         :param str port: Port to check ipv6only=on directive for
 
         :returns: Tuple containing information if IPv6 is enabled in the global
@@ -395,7 +393,7 @@
             for addr in vh.addrs:
                 if addr.ipv6:
                     ipv6_active = True
-                if addr.ipv6only and addr.get_port() == port:
+                if addr.ipv6only and addr.get_port() == port and 
addr.get_addr() == host:
                     ipv6only_present = True
         return ipv6_active, ipv6only_present
 
@@ -479,9 +477,9 @@
             # Wildcard match - need to find the longest one
             rank = matches[0]['rank']
             wildcards = [x for x in matches if x['rank'] == rank]
-            return max(wildcards, key=lambda x: len(x['name']))['vhost']
+            return cast(obj.VirtualHost, max(wildcards, key=lambda x: 
len(x['name']))['vhost'])
         # Exact or regex match
-        return matches[0]['vhost']
+        return cast(obj.VirtualHost, matches[0]['vhost'])
 
     def _rank_matches_by_name(self, vhost_list: Iterable[obj.VirtualHost],
                               target_name: str) -> List[Dict[str, Any]]:
@@ -691,7 +689,7 @@
                         else:
                             socket.inet_pton(socket.AF_INET, host)
                         all_names.add(socket.gethostbyaddr(host)[0])
-                    except (socket.error, socket.herror, socket.timeout):
+                    except (OSError, socket.herror, socket.timeout):
                         continue
 
         return util.get_filtered_names(all_names)
@@ -701,14 +699,16 @@
         # TODO: generate only once
         tmp_dir = os.path.join(self.config.work_dir, "snakeoil")
         le_key = crypto_util.generate_key(
-            key_size=2048, key_dir=tmp_dir, keyname="key.pem",
+            key_type='rsa', key_size=2048, key_dir=tmp_dir, keyname="key.pem",
             strict_permissions=self.config.strict_permissions)
         assert le_key.file is not None
-        key = OpenSSL.crypto.load_privatekey(
-            OpenSSL.crypto.FILETYPE_PEM, le_key.pem)
-        cert = acme_crypto_util.gen_ss_cert(key, 
domains=[socket.gethostname()])
-        cert_pem = OpenSSL.crypto.dump_certificate(
-            OpenSSL.crypto.FILETYPE_PEM, cert)
+        cryptography_key = serialization.load_pem_private_key(le_key.pem, 
password=None)
+        assert isinstance(cryptography_key, rsa.RSAPrivateKey)
+        cert = acme_crypto_util.make_self_signed_cert(
+            cryptography_key,
+            domains=[socket.gethostname()]
+        )
+        cert_pem = cert.public_bytes(serialization.Encoding.PEM)
         cert_file, cert_path = util.unique_file(
             os.path.join(tmp_dir, "cert.pem"), mode="wb")
         with cert_file:
@@ -725,9 +725,16 @@
 
         """
         https_port = self.config.https_port
-        ipv6info = self.ipv6_info(str(https_port))
-        ipv6_block = ['']
-        ipv4_block = ['']
+        http_port = self.config.http01_port
+
+        # no addresses should have ssl turned on here
+        assert not vhost.ssl
+
+        addrs_to_insert: List[obj.Addr] = [
+            obj.Addr.fromstring(f'{addr.get_addr()}:{https_port} ssl')
+            for addr in vhost.addrs
+            if addr.get_port() == str(http_port)
+        ]
 
         # If the vhost was implicitly listening on the default Nginx port,
         # have it continue to do so.
@@ -735,31 +742,46 @@
             listen_block = [['\n    ', 'listen', ' ', 
self.DEFAULT_LISTEN_PORT]]
             self.parser.add_server_directives(vhost, listen_block)
 
-        if vhost.ipv6_enabled():
-            ipv6_block = ['\n    ',
-                          'listen',
-                          ' ',
-                          '[::]:{0}'.format(https_port),
-                          ' ',
-                          'ssl']
-            if not ipv6info[1]:
-                # ipv6only=on is absent in global config
-                ipv6_block.append(' ')
-                ipv6_block.append('ipv6only=on')
-
-        if vhost.ipv4_enabled():
-            ipv4_block = ['\n    ',
-                          'listen',
-                          ' ',
-                          '{0}'.format(https_port),
-                          ' ',
-                          'ssl']
+        if not addrs_to_insert:
+            # there are no existing addresses listening on 80
+            if vhost.ipv6_enabled():
+                addrs_to_insert += [obj.Addr.fromstring(f'[::]:{https_port} 
ssl')]
+            if vhost.ipv4_enabled():
+                addrs_to_insert += [obj.Addr.fromstring(f'{https_port} ssl')]
+
+        addr_blocks: List[List[str]] = []
+        ipv6only_set_here: Set[Tuple[str, str]] = set()
+        for addr in addrs_to_insert:
+            host = addr.get_addr()
+            port = addr.get_port()
+            if addr.ipv6:
+                addr_block = ['\n    ',
+                              'listen',
+                              ' ',
+                              f'{host}:{port}',
+                              ' ',
+                              'ssl']
+                ipv6only_exists = self.ipv6_info(host, port)[1]
+                if not ipv6only_exists and (host, port) not in 
ipv6only_set_here:
+                    addr.ipv6only = True # bookkeeping in case we switch 
output implementation
+                    ipv6only_set_here.add((host, port))
+                    addr_block.append(' ')
+                    addr_block.append('ipv6only=on')
+                addr_blocks.append(addr_block)
+            else:
+                tuple_string = f'{host}:{port}' if host else f'{port}'
+                addr_block = ['\n    ',
+                              'listen',
+                              ' ',
+                              tuple_string,
+                              ' ',
+                              'ssl']
+                addr_blocks.append(addr_block)
 
         snakeoil_cert, snakeoil_key = self._get_snakeoil_paths()
 
         ssl_block = ([
-            ipv6_block,
-            ipv4_block,
+            *addr_blocks,
             ['\n    ', 'ssl_certificate', ' ', snakeoil_cert],
             ['\n    ', 'ssl_certificate_key', ' ', snakeoil_key],
             ['\n    ', 'include', ' ', self.mod_ssl_conf],
@@ -1096,7 +1118,7 @@
         """
         text = self._nginx_version()
 
-        matches = re.findall(r"running with OpenSSL ([^ ]+) ", text)
+        matches: List[str] = re.findall(r"running with OpenSSL ([^ ]+) ", text)
         if not matches:
             matches = re.findall(r"built with OpenSSL ([^ ]+) ", text)
             if not matches:
diff -Nru python-certbot-nginx-2.9.0/certbot_nginx/_internal/http_01.py 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/http_01.py
--- python-certbot-nginx-2.9.0/certbot_nginx/_internal/http_01.py       
2024-02-08 14:45:17.000000000 -0500
+++ python-certbot-nginx-4.0.0/certbot_nginx/_internal/http_01.py       
2025-04-07 18:03:33.000000000 -0400
@@ -1,6 +1,5 @@
 """A class that performs HTTP-01 challenges for Nginx"""
 
-import io
 import logging
 from typing import Any
 from typing import List
@@ -78,11 +77,11 @@
         """
         included = False
         include_directive = ['\n', 'include', ' ', self.challenge_conf]
-        root = self.configurator.parser.config_root
+        http_path = self.configurator.parser.http_path
 
         bucket_directive = ['\n', 'server_names_hash_bucket_size', ' ', '128']
 
-        main = self.configurator.parser.parsed[root]
+        main = self.configurator.parser.parsed[http_path]
         # insert include directive
         for line in main:
             if line[0] == ['http']:
@@ -127,6 +126,7 @@
                     body.insert(0, bucket_directive)
                     break
 
+        root = self.configurator.parser.config_root
         if not included:
             raise errors.MisconfigurationError(
                 'Certbot could not find a block to include '
@@ -139,7 +139,7 @@
         self.configurator.reverter.register_file_creation(
             True, self.challenge_conf)
 
-        with io.open(self.challenge_conf, "w", encoding="utf-8") as new_conf:
+        with open(self.challenge_conf, "w", encoding="utf-8") as new_conf:
             nginxparser.dump(config, new_conf)
 
     def _default_listen_addresses(self) -> List[Addr]:
@@ -147,13 +147,13 @@
         :returns: list of :class:`certbot_nginx._internal.obj.Addr` to apply
         :rtype: list
         """
-        addresses: List[Optional[Addr]] = []
+        addresses: List[Addr] = []
         default_addr = "%s" % self.configurator.config.http01_port
         ipv6_addr = "[::]:{0}".format(
             self.configurator.config.http01_port)
         port = self.configurator.config.http01_port
 
-        ipv6, ipv6only = self.configurator.ipv6_info(str(port))
+        ipv6, ipv6only = self.configurator.ipv6_info("[::]", str(port))
 
         if ipv6:
             # If IPv6 is active in Nginx configuration
@@ -170,7 +170,7 @@
             logger.debug("Using default address %s for authentication.",
                         default_addr)
 
-        return [address for address in addresses if address]
+        return addresses
 
     def _get_validation_path(self, achall: KeyAuthorizationAnnotatedChallenge) 
-> str:
         return os.sep + os.path.join(challenges.HTTP01.URI_ROOT_PATH, 
achall.chall.encode("token"))
diff -Nru python-certbot-nginx-2.9.0/certbot_nginx/_internal/nginxparser.py 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/nginxparser.py
--- python-certbot-nginx-2.9.0/certbot_nginx/_internal/nginxparser.py   
2024-02-08 14:45:17.000000000 -0500
+++ python-certbot-nginx-4.0.0/certbot_nginx/_internal/nginxparser.py   
2025-04-07 18:03:33.000000000 -0400
@@ -35,8 +35,8 @@
     """A class that parses nginx configuration with pyparsing."""
 
     # constants
-    space = Optional(White()).leaveWhitespace()
-    required_space = White().leaveWhitespace()
+    space = Optional(White(ws=' \t\r\n\u00a0')).leaveWhitespace()
+    required_space = White(ws=' \t\r\n\u00a0').leaveWhitespace()
 
     left_bracket = Literal("{").suppress()
     right_bracket = space + Literal("}").suppress()
@@ -102,8 +102,7 @@
             if isinstance(item[0], list): # block
                 yield "".join(item.pop(0)) + '{'
                 for parameter in item.pop(0):
-                    for line in self.__iter__([parameter]): # negate "for b0 
in blocks"
-                        yield line
+                    yield from self.__iter__([parameter]) # negate "for b0 in 
blocks"
                 yield '}'
             else: # not a block - list of strings
                 semicolon = ";"
@@ -294,7 +293,7 @@
     """Dump to a Unicode string.
 
     :param UnspacedList blocks: The parsed tree
-    :rtype: six.text_type
+    :rtype: str
 
     """
     return str(RawNginxDumper(blocks.spaced))
diff -Nru python-certbot-nginx-2.9.0/certbot_nginx/_internal/obj.py 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/obj.py
--- python-certbot-nginx-2.9.0/certbot_nginx/_internal/obj.py   2024-02-08 
14:45:17.000000000 -0500
+++ python-certbot-nginx-4.0.0/certbot_nginx/_internal/obj.py   2025-04-07 
18:03:33.000000000 -0400
@@ -12,6 +12,10 @@
 ADD_HEADER_DIRECTIVE = 'add_header'
 
 
+class SocketAddrError(Exception):
+    """Raised when a UNIX-domain socket address is encountered."""
+
+
 class Addr(common.Addr):
     r"""Represents an Nginx address, i.e. what comes after the 'listen'
     directive.
@@ -51,8 +55,15 @@
         self.unspecified_address = host in self.UNSPECIFIED_IPV4_ADDRESSES
 
     @classmethod
-    def fromstring(cls, str_addr: str) -> Optional["Addr"]:
-        """Initialize Addr from string."""
+    def fromstring(cls, str_addr: str) -> "Addr":
+        """Initialize Addr from string.
+
+        :param str str_addr: nginx address string
+        :returns: parsed nginx address
+        :rtype: Addr
+        :raises SocketAddrError: if a UNIX-domain socket address is given
+
+        """
         parts = str_addr.split(' ')
         ssl = False
         default = False
@@ -64,9 +75,9 @@
         # The first part must be the address
         addr = parts.pop(0)
 
-        # Ignore UNIX-domain sockets
+        # Raise for UNIX-domain sockets
         if addr.startswith('unix:'):
-            return None
+            raise SocketAddrError(f'encountered UNIX-domain socket address 
{str_addr}')
 
         # IPv6 check
         ipv6_match = re.match(r'\[.*\]', addr)
diff -Nru python-certbot-nginx-2.9.0/certbot_nginx/_internal/parser_obj.py 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/parser_obj.py
--- python-certbot-nginx-2.9.0/certbot_nginx/_internal/parser_obj.py    
2024-02-08 14:45:17.000000000 -0500
+++ python-certbot-nginx-4.0.0/certbot_nginx/_internal/parser_obj.py    
2025-04-07 18:03:33.000000000 -0400
@@ -185,8 +185,7 @@
                 match: Optional[Callable[["Parsable"], bool]] = None) -> 
Iterator[Any]:
         """ Combines each statement's iterator.  """
         for elem in self._data:
-            for sub_elem in elem.iterate(expanded, match):
-                yield sub_elem
+            yield from elem.iterate(expanded, match)
 
     # ======== End overridden functions
 
@@ -310,8 +309,7 @@
         if match is None or match(self):
             yield self
         if expanded:
-            for elem in self.contents.iterate(expanded, match):
-                yield elem
+            yield from self.contents.iterate(expanded, match)
 
     def parse(self, raw_list: List[Any], add_spaces: bool = False) -> None:
         """ Parses a list that resembles a block.
diff -Nru python-certbot-nginx-2.9.0/certbot_nginx/_internal/parser.py 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/parser.py
--- python-certbot-nginx-2.9.0/certbot_nginx/_internal/parser.py        
2024-02-08 14:45:17.000000000 -0500
+++ python-certbot-nginx-4.0.0/certbot_nginx/_internal/parser.py        
2025-04-07 18:03:33.000000000 -0400
@@ -1,8 +1,8 @@
 """NginxParser is a member object of the NginxConfigurator class."""
+from __future__ import annotations
 import copy
 import functools
 import glob
-import io
 import logging
 import re
 from typing import Any
@@ -42,6 +42,7 @@
         self.parsed: Dict[str, UnspacedList] = {}
         self.root = os.path.abspath(root)
         self.config_root = self._find_config_root()
+        self._http_path: str | None = None
 
         # Parse nginx.conf and included files.
         # TODO: Check sites-available/ as well. For now, the configurator does
@@ -55,6 +56,14 @@
         self.parsed = {}
         self._parse_recursively(self.config_root)
 
+    @property
+    def http_path(self) -> str:
+        """ Filepath of file containing nginx http block. Set in 
self._parse_recursively
+        """
+        if self._http_path is None:
+            raise errors.MisconfigurationError('No nginx http block found')
+        return self._http_path
+
     def _parse_recursively(self, filepath: str) -> None:
         """Parses nginx config files recursively by looking at 'include'
         directives inside 'http' and 'server' blocks. Note that this only
@@ -65,13 +74,16 @@
         """
         # pylint: disable=too-many-nested-blocks
         filepath = self.abs_path(filepath)
-        trees = self._parse_files(filepath)
-        for tree in trees:
+        trees: dict[str, UnspacedList] = self._parse_files(filepath)
+        for filename, tree in trees.items():
             for entry in tree:
                 if _is_include_directive(entry):
                     # Parse the top-level included file
                     self._parse_recursively(entry[1])
                 elif entry[0] == ['http'] or entry[0] == ['server']:
+                    # Note http block location for http_01.py
+                    if entry[0] == ['http']:
+                        self._http_path = filename
                     # Look for includes in the top-level 'http'/'server' 
context
                     for subentry in entry[1]:
                         if _is_include_directive(subentry):
@@ -194,35 +206,35 @@
                         pass
         return result
 
-    def _parse_files(self, filepath: str, override: bool = False) -> 
List[UnspacedList]:
+    def _parse_files(self, filepath: str, override: bool = False) -> dict[str, 
UnspacedList]:
         """Parse files from a glob
 
         :param str filepath: Nginx config file path
         :param bool override: Whether to parse a file that has been parsed
-        :returns: list of parsed tree structures
-        :rtype: list
+        :returns: dict of parsed tree structures indexed by filename
+        :rtype: dict[str, UnspacedList]
 
         """
-        files = glob.glob(filepath) # nginx on unix calls glob(3) for this
+        files: list[str] = glob.glob(filepath) # nginx on unix calls glob(3) 
for this
                                     # XXX Windows nginx uses FindFirstFile, and
                                     # should have a narrower call here
-        trees = []
-        for item in files:
-            if item in self.parsed and not override:
+        trees: dict[str, UnspacedList] = {}
+        for filename in files:
+            if filename in self.parsed and not override:
                 continue
             try:
-                with io.open(item, "r", encoding="utf-8") as _file:
+                with open(filename, "r", encoding="utf-8") as _file:
                     parsed = nginxparser.load(_file)
-                    self.parsed[item] = parsed
-                    trees.append(parsed)
-            except IOError:
-                logger.warning("Could not open file: %s", item)
+                    self.parsed[filename] = parsed
+                    trees[filename] = parsed
+            except OSError:
+                logger.warning("Could not open file: %s", filename)
             except UnicodeDecodeError:
                 logger.warning("Could not read file: %s due to invalid "
                                "character. Only UTF-8 encoding is "
-                               "supported.", item)
+                               "supported.", filename)
             except pyparsing.ParseException as err:
-                logger.warning("Could not parse file: %s due to %s", item, err)
+                logger.warning("Could not parse file: %s due to %s", filename, 
err)
         return trees
 
     def _find_config_root(self) -> str:
@@ -255,10 +267,10 @@
                     continue
                 out = nginxparser.dumps(tree)
                 logger.debug('Writing nginx conf tree to %s:\n%s', filename, 
out)
-                with io.open(filename, 'w', encoding='utf-8') as _file:
+                with open(filename, 'w', encoding='utf-8') as _file:
                     _file.write(out)
 
-            except IOError:
+            except OSError:
                 logger.error("Could not open file for writing: %s", filename)
 
     def parse_server(self, server: UnspacedList) -> Dict[str, Any]:
@@ -431,9 +443,9 @@
 def _parse_ssl_options(ssl_options: Optional[str]) -> List[UnspacedList]:
     if ssl_options is not None:
         try:
-            with io.open(ssl_options, "r", encoding="utf-8") as _file:
+            with open(ssl_options, "r", encoding="utf-8") as _file:
                 return nginxparser.load(_file)
-        except IOError:
+        except OSError:
             logger.warning("Missing NGINX TLS options file: %s", ssl_options)
         except UnicodeDecodeError:
             logger.warning("Could not read file: %s due to invalid character. "
@@ -795,13 +807,20 @@
         if not directive:
             continue
         if directive[0] == 'listen':
-            addr = obj.Addr.fromstring(" ".join(directive[1:]))
-            if addr:
-                addrs.add(addr)
-                if addr.ssl:
-                    ssl = True
+            try:
+                addr = obj.Addr.fromstring(" ".join(directive[1:]))
+            except obj.SocketAddrError:
+                # Ignore UNIX-domain socket addresses
+                continue
+            addrs.add(addr)
+            if addr.ssl:
+                ssl = True
         elif directive[0] == 'server_name':
-            names.update(x.strip('"\'') for x in directive[1:])
+            params = directive[1:]
+            while '#' in params:
+                end_index = [i for i, param in enumerate(params) if 
param.startswith('\n')][0]
+                params = params[:params.index('#')] + params[end_index+1:]
+            names.update(x.strip('"\'') for x in params)
         elif _is_ssl_on_directive(directive):
             ssl = True
             apply_ssl_to_all_addrs = True
diff -Nru 
python-certbot-nginx-2.9.0/certbot_nginx/_internal/tests/configurator_test.py 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/tests/configurator_test.py
--- 
python-certbot-nginx-2.9.0/certbot_nginx/_internal/tests/configurator_test.py   
    2024-02-08 14:45:17.000000000 -0500
+++ 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/tests/configurator_test.py   
    2025-04-07 18:03:33.000000000 -0400
@@ -2,9 +2,11 @@
 import sys
 from unittest import mock
 
-import OpenSSL
 import pytest
 
+from cryptography import x509
+from cryptography.hazmat.primitives import serialization
+
 from acme import challenges
 from acme import messages
 from certbot import achallenges
@@ -40,7 +42,7 @@
 
     def test_prepare(self):
         assert (1, 6, 2) == self.config.version
-        assert 14 == len(self.config.parser.parsed)
+        assert 15 == len(self.config.parser.parsed)
 
     @mock.patch("certbot_nginx._internal.configurator.util.exe_exists")
     @mock.patch("certbot_nginx._internal.configurator.subprocess.run")
@@ -98,7 +100,7 @@
             "155.225.50.69.nephoscale.net", "www.example.org", "another.alias",
              "migration.com", "summer.com", "geese.com", "sslon.com",
              "globalssl.com", "globalsslsetssl.com", "ipv6.com", "ipv6ssl.com",
-             "headers.com", "example.net", "ssl.both.com"}
+             "headers.com", "example.net", "ssl.both.com", 'addr-80.com'}
 
     def test_supported_enhancements(self):
         assert ['redirect', 'ensure-http-header', 'staple-ocsp'] == \
@@ -132,7 +134,7 @@
                             ['server_name', 'example.*'],
                             ['listen', '5001', 'ssl'],
                             ['#', parser.COMMENT]]]] == \
-                         parsed[0]
+                         parsed[filep]
 
     def test_choose_vhosts_alias(self):
         self._test_choose_vhosts_common('alias', 'server_conf')
@@ -199,11 +201,38 @@
                 with pytest.raises(errors.MisconfigurationError):
                     self.config.choose_vhosts(name)
 
+    def test_choose_vhosts_keep_ip_address(self):
+        # no listen on port 80
+        # listen       69.50.225.155:9000;
+        # listen       127.0.0.1;
+        vhost = self.config.choose_vhosts('example.com')[0]
+        assert obj.Addr.fromstring("5001 ssl") in vhost.addrs
+
+        # no listens at all
+        vhost = self.config.choose_vhosts('headers.com')[0]
+        assert obj.Addr.fromstring("5001 ssl") in vhost.addrs
+        assert obj.Addr.fromstring("80") in vhost.addrs
+
+        # blank addr listen on 80 should result in blank addr ssl
+        # listen 80;
+        # listen [::]:80;
+        vhost = self.config.choose_vhosts('ipv6.com')[0]
+        assert obj.Addr.fromstring("5001 ssl") in vhost.addrs
+        assert obj.Addr.fromstring("[::]:5001 ssl") in vhost.addrs
+
+        # listen on 80 with ip address should result in copied addr
+        # listen 1.2.3.4:80;
+        # listen [1:20::300]:80;
+        vhost = self.config.choose_vhosts('addr-80.com')[0]
+        assert obj.Addr.fromstring("1.2.3.4:5001 ssl") in vhost.addrs
+        assert obj.Addr.fromstring("[1:20::300]:5001 ssl ipv6only=on") in 
vhost.addrs
+
+
     def test_ipv6only(self):
         # ipv6_info: (ipv6_active, ipv6only_present)
-        assert (True, False) == self.config.ipv6_info("80")
+        assert (True, False) == self.config.ipv6_info("[::]", "80")
         # Port 443 has ipv6only=on because of ipv6ssl.com vhost
-        assert (True, True) == self.config.ipv6_info("443")
+        assert (True, True) == self.config.ipv6_info("[::]", "443")
 
     def test_ipv6only_detection(self):
         self.config.version = (1, 3, 1)
@@ -545,12 +574,10 @@
         cert, key = self.config._get_snakeoil_paths()
         assert os.path.exists(cert)
         assert os.path.exists(key)
-        with open(cert) as cert_file:
-            OpenSSL.crypto.load_certificate(
-                OpenSSL.crypto.FILETYPE_PEM, cert_file.read())
-        with open(key) as key_file:
-            OpenSSL.crypto.load_privatekey(
-                OpenSSL.crypto.FILETYPE_PEM, key_file.read())
+        with open(cert, "rb") as cert_file:
+            x509.load_pem_x509_certificate(cert_file.read())
+        with open(key, "rb") as key_file:
+            serialization.load_pem_private_key(key_file.read(), password=None)
 
     def test_redirect_enhance(self):
         # Test that we successfully add a redirect when there is
@@ -585,7 +612,7 @@
         generated_conf = self.config.parser.parsed[example_conf]
         assert [[['server'], [
                ['server_name', '.example.com'],
-               ['server_name', 'example.*'], [],
+               ['server_name', 'example.*'],
                ['listen', '5001', 'ssl'], ['#', ' managed by Certbot'],
                ['ssl_certificate', 'example/fullchain.pem'], ['#', ' managed 
by Certbot'],
                ['ssl_certificate_key', 'example/key.pem'], ['#', ' managed by 
Certbot'],
@@ -615,7 +642,7 @@
         generated_conf = self.config.parser.parsed[example_conf]
         assert [[['server'], [
                ['server_name', '.example.com'],
-               ['server_name', 'example.*'], [],
+               ['server_name', 'example.*'],
                ['listen', '5001', 'ssl'], ['#', ' managed by Certbot'],
                ['ssl_certificate', 'example/fullchain.pem'], ['#', ' managed 
by Certbot'],
                ['ssl_certificate_key', 'example/key.pem'], ['#', ' managed by 
Certbot'],
@@ -630,7 +657,7 @@
                ['listen', '127.0.0.1'],
                ['server_name', '.example.com'],
                ['server_name', 'example.*'],
-               [], [], []]]] == \
+               [], []]]] == \
             generated_conf
 
     def test_http_header_hsts(self):
@@ -957,7 +984,7 @@
                                                 prefer_ssl=False,
                                                 no_ssl_filter_port='80')
             # Check that the dialog was called with only port 80 vhosts
-            assert len(mock_select_vhs.call_args[0][0]) == 8
+            assert len(mock_select_vhs.call_args[0][0]) == 9
 
     def test_choose_auth_vhosts(self):
         """choose_auth_vhosts correctly selects duplicative and HTTP/HTTPS 
vhosts"""
@@ -1074,16 +1101,13 @@
         file has been manually edited by the user, and will refuse to update 
it.
         This test ensures that all necessary hashes are present.
         """
-        if sys.version_info >= (3, 9):  # pragma: no cover
-            import importlib.resources as importlib_resources
-        else:  # pragma: no cover
-            import importlib_resources
-        
+        import importlib.resources
+
         from certbot_nginx._internal.constants import ALL_SSL_OPTIONS_HASHES
 
-        tls_configs_ref = importlib_resources.files("certbot_nginx").joinpath(
+        tls_configs_ref = importlib.resources.files("certbot_nginx").joinpath(
             "_internal", "tls_configs")
-        with importlib_resources.as_file(tls_configs_ref) as tls_configs_dir:
+        with importlib.resources.as_file(tls_configs_ref) as tls_configs_dir:
             for tls_config_file in os.listdir(tls_configs_dir):
                 file_hash = 
crypto_util.sha256sum(os.path.join(tls_configs_dir, tls_config_file))
                 assert file_hash in ALL_SSL_OPTIONS_HASHES, \
diff -Nru 
python-certbot-nginx-2.9.0/certbot_nginx/_internal/tests/nginxparser_test.py 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/tests/nginxparser_test.py
--- 
python-certbot-nginx-2.9.0/certbot_nginx/_internal/tests/nginxparser_test.py    
    2024-02-08 14:45:17.000000000 -0500
+++ 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/tests/nginxparser_test.py    
    2025-04-07 18:03:33.000000000 -0400
@@ -362,6 +362,18 @@
         parsed = loads("")
         assert parsed == []
 
+    def test_non_breaking_spaces(self):
+        # non-breaking spaces
+        test = u'\u00a0'
+        loads(test)
+        test = """
+        map $http_upgrade $connection_upgrade {
+            default upgrade;
+            ''      close;
+        }
+        """
+        loads(test)
+
 
 class TestUnspacedList(unittest.TestCase):
     """Test the UnspacedList data structure"""
diff -Nru python-certbot-nginx-2.9.0/certbot_nginx/_internal/tests/obj_test.py 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/tests/obj_test.py
--- python-certbot-nginx-2.9.0/certbot_nginx/_internal/tests/obj_test.py        
2024-02-08 14:45:17.000000000 -0500
+++ python-certbot-nginx-4.0.0/certbot_nginx/_internal/tests/obj_test.py        
2025-04-07 18:03:33.000000000 -0400
@@ -16,8 +16,7 @@
         self.addr4 = Addr.fromstring("*:80 default_server ssl")
         self.addr5 = Addr.fromstring("myhost")
         self.addr6 = Addr.fromstring("80 default_server spdy")
-        self.addr7 = Addr.fromstring("unix:/var/run/nginx.sock")
-        self.addr8 = Addr.fromstring("*:80 default ssl")
+        self.addr7 = Addr.fromstring("*:80 default ssl")
 
     def test_fromstring(self):
         assert self.addr1.get_addr() == "192.168.1.1"
@@ -50,9 +49,13 @@
         assert self.addr6.ssl is False
         assert self.addr6.default is True
 
-        assert self.addr8.default is True
+        assert self.addr7.default is True
 
-        assert self.addr7 is None
+    def test_fromstring_socket(self):
+        from certbot_nginx._internal.obj import Addr, SocketAddrError
+        socket_string = r"unix:/var/run/nginx.sock"
+        with pytest.raises(SocketAddrError, match=socket_string):
+            Addr.fromstring(socket_string)
 
     def test_str(self):
         assert str(self.addr1) == "192.168.1.1"
@@ -61,7 +64,7 @@
         assert str(self.addr4) == "*:80 default_server ssl"
         assert str(self.addr5) == "myhost"
         assert str(self.addr6) == "80 default_server"
-        assert str(self.addr8) == "*:80 default_server ssl"
+        assert str(self.addr7) == "*:80 default_server ssl"
 
     def test_to_string(self):
         assert self.addr1.to_string() == "192.168.1.1"
diff -Nru 
python-certbot-nginx-2.9.0/certbot_nginx/_internal/tests/parser_test.py 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/tests/parser_test.py
--- python-certbot-nginx-2.9.0/certbot_nginx/_internal/tests/parser_test.py     
2024-02-08 14:45:17.000000000 -0500
+++ python-certbot-nginx-4.0.0/certbot_nginx/_internal/tests/parser_test.py     
2025-04-07 18:03:33.000000000 -0400
@@ -62,7 +62,8 @@
                            'sites-enabled/globalssl.com',
                            'sites-enabled/ipv6.com',
                            'sites-enabled/ipv6ssl.com',
-                           'sites-enabled/example.net']} == \
+                           'sites-enabled/example.net',
+                           'sites-enabled/addr-80.com']} == \
                          set(nparser.parsed.keys())
         assert [['server_name', 'somename', 'alias', 'another.alias']] == \
                          nparser.parsed[nparser.abs_path('server.conf')]
@@ -73,6 +74,15 @@
                          nparser.parsed[nparser.abs_path(
                              'sites-enabled/example.com')]
 
+    def test_included_load(self):
+        # Test for when the root file doesn't have http in it
+        nparser = parser.NginxParser(self.config_path)
+        nparser.config_root = os.path.join(self.config_path, 
"nginx-include.conf")
+        nparser.load()
+        assert len(nparser.parsed) > 1
+        assert len(nparser.parsed[nparser.config_root]) == 4
+        assert os.path.join(self.config_path, "nginx.conf") in nparser.parsed
+
     def test_abs_path(self):
         nparser = parser.NginxParser(self.config_path)
         if os.name != 'nt':
@@ -92,13 +102,13 @@
         parsed = nparser._parse_files(nparser.abs_path(
             'sites-enabled/example.com.test'))
         assert 4 == len(glob.glob(nparser.abs_path('*.test')))
-        assert 10 == len(
+        assert 11 == len(
             glob.glob(nparser.abs_path('sites-enabled/*.test')))
         assert [[['server'], [['listen', '69.50.225.155:9000'],
                                         ['listen', '127.0.0.1'],
                                         ['server_name', '.example.com'],
                                         ['server_name', 'example.*']]]] == \
-                         parsed[0]
+                         
parsed[nparser.abs_path('sites-enabled/example.com.test')]
 
     def test__do_for_subarray(self):
         # pylint: disable=protected-access
@@ -175,7 +185,7 @@
                                                   '*.www.example.com'},
                                  [], [2, 1, 0])
 
-        assert 19 == len(vhosts)
+        assert 20 == len(vhosts)
         example_com = [x for x in vhosts if 'example.com' in x.filep][0]
         assert vhost3 == example_com
         default = [x for x in vhosts if 'default' in x.filep][0]
@@ -428,6 +438,60 @@
         ])
         assert server['ssl']
 
+    def test_parse_server_raw_comment(self):
+        testdata = """
+        server_name *.goo.far
+            # commented
+            baz.com;
+        """
+        loaded = nginxparser.loads(testdata)
+        server = parser._parse_server_raw(loaded) #pylint: 
disable=protected-access
+        assert server['names'] == {'*.goo.far', 'baz.com'}
+
+        testdata = """
+        server_name *.goo.far # commented
+            baz.com;
+        """
+        loaded = nginxparser.loads(testdata)
+        server = parser._parse_server_raw(loaded) #pylint: 
disable=protected-access
+        assert server['names'] == {'*.goo.far', 'baz.com'}
+
+        testdata = """
+        server_name *.goo.far # commented
+            ;
+        """
+        loaded = nginxparser.loads(testdata)
+        server = parser._parse_server_raw(loaded) #pylint: 
disable=protected-access
+        assert server['names'] == {'*.goo.far'}
+
+        # known bug; see https://github.com/certbot/certbot/issues/9942
+        testdata = """
+        server_name *.goo.far
+            #commented
+            ;
+        """
+        loaded = nginxparser.loads(testdata)
+        server = parser._parse_server_raw(loaded) #pylint: 
disable=protected-access
+        assert server['names'] == {'*.goo.far', '#commented'}
+
+        # same bug; # isn't actually allowed in domains
+        testdata = """
+        server_name *.go#o.far
+            ;
+        """
+        loaded = nginxparser.loads(testdata)
+        server = parser._parse_server_raw(loaded) #pylint: 
disable=protected-access
+        assert server['names'] == {'*.go#o.far'}
+
+        testdata = """
+        listen 443
+            # commented
+            ssl;
+        """
+        loaded = nginxparser.loads(testdata)
+        server = parser._parse_server_raw(loaded) #pylint: 
disable=protected-access
+        assert server['addrs'] == {obj.Addr.fromstring('443 ssl')}
+
     def test_parse_server_raw_unix(self):
         server = parser._parse_server_raw([ #pylint: disable=protected-access
             ['listen', 'unix:/var/run/nginx.sock']
@@ -489,8 +553,8 @@
         nparser = parser.NginxParser(self.config_path)
         path = nparser.abs_path('valid_unicode_comments.conf')
         parsed = nparser._parse_files(path)  # pylint: disable=protected-access
-        assert ['server'] == parsed[0][2][0]
-        assert ['listen', '80'] == parsed[0][2][1][3]
+        assert ['server'] == parsed[path][2][0]
+        assert ['listen', '80'] == parsed[path][2][1][3]
 
     def test_valid_unicode_roundtrip(self):
         """This tests the parser's ability to load and save a config 
containing Unicode"""
@@ -506,7 +570,7 @@
             path = nparser.abs_path('invalid_unicode_comments.conf')
             parsed = nparser._parse_files(path)  # pylint: 
disable=protected-access
 
-        assert [] == parsed
+        assert {} == parsed
         assert any(
             ('invalid character' in output) and ('UTF-8' in output)
             for output in log.output
diff -Nru 
python-certbot-nginx-2.9.0/certbot_nginx/_internal/tests/testdata/etc_nginx/nginx-include.conf
 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/tests/testdata/etc_nginx/nginx-include.conf
--- 
python-certbot-nginx-2.9.0/certbot_nginx/_internal/tests/testdata/etc_nginx/nginx-include.conf
      1969-12-31 19:00:00.000000000 -0500
+++ 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/tests/testdata/etc_nginx/nginx-include.conf
      2025-04-07 18:03:33.000000000 -0400
@@ -0,0 +1,4 @@
+# just a comment on top
+# so we're not at the very top
+include nginx.conf;
+# and another comment for fun
diff -Nru 
python-certbot-nginx-2.9.0/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/addr-80.com
 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/addr-80.com
--- 
python-certbot-nginx-2.9.0/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/addr-80.com
       1969-12-31 19:00:00.000000000 -0500
+++ 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/addr-80.com
       2025-04-07 18:03:33.000000000 -0400
@@ -0,0 +1,5 @@
+server {
+    listen 1.2.3.4:80;
+    listen [1:20::300]:80;
+    server_name addr-80.com;
+}
diff -Nru python-certbot-nginx-2.9.0/certbot_nginx/_internal/tests/test_util.py 
python-certbot-nginx-4.0.0/certbot_nginx/_internal/tests/test_util.py
--- python-certbot-nginx-2.9.0/certbot_nginx/_internal/tests/test_util.py       
2024-02-08 14:45:17.000000000 -0500
+++ python-certbot-nginx-4.0.0/certbot_nginx/_internal/tests/test_util.py       
2025-04-07 18:03:33.000000000 -0400
@@ -1,5 +1,6 @@
 """Common utilities for certbot_nginx."""
 import copy
+import importlib.resources
 import shutil
 import tempfile
 import sys
@@ -15,11 +16,6 @@
 from certbot_nginx._internal import configurator
 from certbot_nginx._internal import nginxparser
 
-if sys.version_info >= (3, 9):  # pragma: no cover
-    import importlib.resources as importlib_resources
-else:  # pragma: no cover
-    import importlib_resources
-
 class NginxTest(test_util.ConfigTestCase):
 
     def setUp(self):
@@ -86,8 +82,8 @@
 @contextmanager
 def get_data_filename(filename):
     """Gets the filename of a test data file."""
-    ref = importlib_resources.files(__package__) / "testdata" / "etc_nginx"/ 
filename
-    with importlib_resources.as_file(ref) as path:
+    ref = importlib.resources.files(__package__) / "testdata" / "etc_nginx"/ 
filename
+    with importlib.resources.as_file(ref) as path:
         yield path
 
 
diff -Nru python-certbot-nginx-2.9.0/certbot_nginx.egg-info/PKG-INFO 
python-certbot-nginx-4.0.0/certbot_nginx.egg-info/PKG-INFO
--- python-certbot-nginx-2.9.0/certbot_nginx.egg-info/PKG-INFO  2024-02-08 
14:45:26.000000000 -0500
+++ python-certbot-nginx-4.0.0/certbot_nginx.egg-info/PKG-INFO  2025-04-07 
18:03:36.000000000 -0400
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: certbot-nginx
-Version: 2.9.0
+Version: 4.0.0
 Summary: Nginx plugin for Certbot
 Home-page: https://github.com/certbot/certbot
 Author: Certbot Project
@@ -13,24 +13,32 @@
 Classifier: Operating System :: POSIX :: Linux
 Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
 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.8
+Requires-Python: >=3.9
 License-File: LICENSE.txt
-Requires-Dist: acme>=2.9.0
-Requires-Dist: certbot>=2.9.0
-Requires-Dist: importlib_resources>=1.3.1; python_version < "3.9"
-Requires-Dist: PyOpenSSL!=23.1.0,>=17.5.0
-Requires-Dist: pyparsing>=2.2.1
-Requires-Dist: setuptools>=41.6.0
+Requires-Dist: acme>=4.0.0
+Requires-Dist: certbot>=4.0.0
+Requires-Dist: PyOpenSSL>=25.0.0
+Requires-Dist: pyparsing>=2.4.7
 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 -Nru python-certbot-nginx-2.9.0/certbot_nginx.egg-info/requires.txt 
python-certbot-nginx-4.0.0/certbot_nginx.egg-info/requires.txt
--- python-certbot-nginx-2.9.0/certbot_nginx.egg-info/requires.txt      
2024-02-08 14:45:26.000000000 -0500
+++ python-certbot-nginx-4.0.0/certbot_nginx.egg-info/requires.txt      
2025-04-07 18:03:36.000000000 -0400
@@ -1,11 +1,7 @@
-acme>=2.9.0
-certbot>=2.9.0
-PyOpenSSL!=23.1.0,>=17.5.0
-pyparsing>=2.2.1
-setuptools>=41.6.0
-
-[:python_version < "3.9"]
-importlib_resources>=1.3.1
+acme>=4.0.0
+certbot>=4.0.0
+PyOpenSSL>=25.0.0
+pyparsing>=2.4.7
 
 [test]
 pytest
diff -Nru python-certbot-nginx-2.9.0/certbot_nginx.egg-info/SOURCES.txt 
python-certbot-nginx-4.0.0/certbot_nginx.egg-info/SOURCES.txt
--- python-certbot-nginx-2.9.0/certbot_nginx.egg-info/SOURCES.txt       
2024-02-08 14:45:26.000000000 -0500
+++ python-certbot-nginx-4.0.0/certbot_nginx.egg-info/SOURCES.txt       
2025-04-07 18:03:36.000000000 -0400
@@ -36,9 +36,11 @@
 certbot_nginx/_internal/tests/testdata/etc_nginx/mime.types
 certbot_nginx/_internal/tests/testdata/etc_nginx/minimalistic_comments.conf
 certbot_nginx/_internal/tests/testdata/etc_nginx/multiline_quotes.conf
+certbot_nginx/_internal/tests/testdata/etc_nginx/nginx-include.conf
 certbot_nginx/_internal/tests/testdata/etc_nginx/nginx.conf
 certbot_nginx/_internal/tests/testdata/etc_nginx/server.conf
 certbot_nginx/_internal/tests/testdata/etc_nginx/valid_unicode_comments.conf
+certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/addr-80.com
 certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/both.com
 certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/default
 certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/example.com
diff -Nru python-certbot-nginx-2.9.0/debian/changelog 
python-certbot-nginx-4.0.0/debian/changelog
--- python-certbot-nginx-2.9.0/debian/changelog 2024-02-13 21:15:37.000000000 
-0500
+++ python-certbot-nginx-4.0.0/debian/changelog 2025-05-25 11:31:25.000000000 
-0400
@@ -1,3 +1,17 @@
+python-certbot-nginx (4.0.0-2) unstable; urgency=medium
+
+  * autopkgtests: drop manual IP flag no longer used
+
+ -- Harlan Lieberman-Berg <hlieber...@debian.org>  Sun, 25 May 2025 11:31:25 
-0400
+
+python-certbot-nginx (4.0.0-1) unstable; urgency=medium
+
+  * d/watch: newer packages use an underscore
+  * New upstream version 4.0.0 (Closes: #1102343, #1106466)
+  * Bump dependency requirements
+
+ -- Harlan Lieberman-Berg <hlieber...@debian.org>  Sat, 24 May 2025 17:19:42 
-0400
+
 python-certbot-nginx (2.9.0-1) unstable; urgency=medium
 
   * New upstream version 2.9.0
diff -Nru python-certbot-nginx-2.9.0/debian/control 
python-certbot-nginx-4.0.0/debian/control
--- python-certbot-nginx-2.9.0/debian/control   2024-02-08 20:25:51.000000000 
-0500
+++ python-certbot-nginx-4.0.0/debian/control   2025-05-24 16:13:22.000000000 
-0400
@@ -6,10 +6,9 @@
 Build-Depends: debhelper-compat (= 13),
                dh-python,
                python3,
-               python3-acme-abi-2 (>= 2.1~),
-               python3-certbot-abi-2 (>= 2.1~),
+               python3-acme-abi-4 (>= 4.0~),
+               python3-certbot-abi-4 (>= 4.0~),
                python3-configargparse,
-               python3-mock,
                python3-openssl,
                python3-parsedatetime,
                python3-pyparsing,
@@ -30,8 +29,8 @@
 Architecture: all
 Depends: certbot,
          nginx,
-         python3-acme-abi-2 (>= ${Abi-major-minor-version}),
-         python3-certbot-abi-2 (>= ${Abi-major-minor-version}),
+         python3-acme-abi-4 (>= ${Abi-major-minor-version}),
+         python3-certbot-abi-4 (>= ${Abi-major-minor-version}),
          ${misc:Depends},
          ${python3:Depends}
 Suggests: python-certbot-nginx-doc
diff -Nru python-certbot-nginx-2.9.0/debian/tests/nginx 
python-certbot-nginx-4.0.0/debian/tests/nginx
--- python-certbot-nginx-2.9.0/debian/tests/nginx       2020-07-24 
01:15:40.000000000 -0400
+++ python-certbot-nginx-4.0.0/debian/tests/nginx       2025-05-25 
11:30:45.000000000 -0400
@@ -48,7 +48,6 @@
     --no-random-sleep-on-renew \
     --server https://localhost:14000/dir \
     --no-verify-ssl \
-    --manual-public-ip-logging-ok \
     --config-dir ${TMP_DIR}/certbot/nginx/conf \
     --work-dir ${TMP_DIR}/certbot/nginx/work \
     --logs-dir ${TMP_DIR}/certbot/nginx/logs \
diff -Nru python-certbot-nginx-2.9.0/debian/watch 
python-certbot-nginx-4.0.0/debian/watch
--- python-certbot-nginx-2.9.0/debian/watch     2021-08-23 18:37:56.000000000 
-0400
+++ python-certbot-nginx-4.0.0/debian/watch     2025-05-24 16:07:53.000000000 
-0400
@@ -1,3 +1,3 @@
 version=4
 opts=uversionmangle=s/(rc|a|b|c)/~$1/,pgpsigurlmangle=s/$/.asc/ \
-https://pypi.debian.net/certbot-nginx/certbot-nginx-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
+https://pypi.debian.net/certbot-nginx/certbot_nginx-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
diff -Nru python-certbot-nginx-2.9.0/PKG-INFO 
python-certbot-nginx-4.0.0/PKG-INFO
--- python-certbot-nginx-2.9.0/PKG-INFO 2024-02-08 14:45:26.255117000 -0500
+++ python-certbot-nginx-4.0.0/PKG-INFO 2025-04-07 18:03:36.122535200 -0400
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: certbot-nginx
-Version: 2.9.0
+Version: 4.0.0
 Summary: Nginx plugin for Certbot
 Home-page: https://github.com/certbot/certbot
 Author: Certbot Project
@@ -13,24 +13,32 @@
 Classifier: Operating System :: POSIX :: Linux
 Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
 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.8
+Requires-Python: >=3.9
 License-File: LICENSE.txt
-Requires-Dist: acme>=2.9.0
-Requires-Dist: certbot>=2.9.0
-Requires-Dist: importlib_resources>=1.3.1; python_version < "3.9"
-Requires-Dist: PyOpenSSL!=23.1.0,>=17.5.0
-Requires-Dist: pyparsing>=2.2.1
-Requires-Dist: setuptools>=41.6.0
+Requires-Dist: acme>=4.0.0
+Requires-Dist: certbot>=4.0.0
+Requires-Dist: PyOpenSSL>=25.0.0
+Requires-Dist: pyparsing>=2.4.7
 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 -Nru python-certbot-nginx-2.9.0/setup.py 
python-certbot-nginx-4.0.0/setup.py
--- python-certbot-nginx-2.9.0/setup.py 2024-02-08 14:45:18.000000000 -0500
+++ python-certbot-nginx-4.0.0/setup.py 2025-04-07 18:03:33.000000000 -0400
@@ -1,7 +1,7 @@
 from setuptools import find_packages
 from setuptools import setup
 
-version = '2.9.0'
+version = '4.0.0'
 
 install_requires = [
     # We specify the minimum acme and certbot version as the current plugin
@@ -9,11 +9,10 @@
     # https://github.com/certbot/certbot/issues/8761 for more info.
     f'acme>={version}',
     f'certbot>={version}',
-    'importlib_resources>=1.3.1; python_version < "3.9"',
-    # pyOpenSSL 23.1.0 is a bad release: 
https://github.com/pyca/pyopenssl/issues/1199
-    'PyOpenSSL>=17.5.0,!=23.1.0',
-    'pyparsing>=2.2.1',
-    'setuptools>=41.6.0',
+    # PyOpenSSL>=25.0.0 is just needed to satisfy mypy right now so this 
dependency can probably be
+    # relaxed to >=24.0.0 if needed.
+    'PyOpenSSL>=25.0.0',
+    'pyparsing>=2.4.7',
 ]
 
 test_extras = [
@@ -28,7 +27,7 @@
     author="Certbot Project",
     author_email='certbot-...@eff.org',
     license='Apache License 2.0',
-    python_requires='>=3.8',
+    python_requires='>=3.9',
     classifiers=[
         'Development Status :: 5 - Production/Stable',
         'Environment :: Plugins',
@@ -37,11 +36,11 @@
         'Operating System :: POSIX :: Linux',
         'Programming Language :: Python',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
         'Programming Language :: Python :: 3.10',
         'Programming Language :: Python :: 3.11',
         'Programming Language :: Python :: 3.12',
+        'Programming Language :: Python :: 3.13',
         'Topic :: Internet :: WWW/HTTP',
         'Topic :: Security',
         'Topic :: System :: Installation/Setup',

Reply via email to