diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index bc5ad90d6..30479f25b 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -15,6 +15,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). * certbot._internal.cli is now a package split in submodules instead of a whole module. * Fix acme module warnings when response Content-Type includes params (e.g. charset). +* Fixed issue where webroot plugin would incorrectly raise `Read-only file system` + error when creating challenge directories (issue #7165). ### Fixed diff --git a/certbot/certbot/_internal/plugins/webroot.py b/certbot/certbot/_internal/plugins/webroot.py index 042b60656..9383ce66d 100644 --- a/certbot/certbot/_internal/plugins/webroot.py +++ b/certbot/certbot/_internal/plugins/webroot.py @@ -1,7 +1,6 @@ """Webroot plugin.""" import argparse import collections -import errno import json import logging @@ -71,7 +70,7 @@ to serve all files under specified web root ({0}).""" super(Authenticator, self).__init__(*args, **kwargs) self.full_roots = {} # type: Dict[str, str] self.performed = collections.defaultdict(set) \ - # type: DefaultDict[str, Set[achallenges.KeyAuthorizationAnnotatedChallenge]] + # type: DefaultDict[str, Set[achallenges.KeyAuthorizationAnnotatedChallenge]] # stack of dirs successfully created by this authenticator self._created_dirs = [] # type: List[str] @@ -137,7 +136,7 @@ to serve all files under specified web root ({0}).""" "webroot when using the webroot plugin.") return None if index == 0 else known_webroots[index - 1] # code == display_util.OK - def _prompt_for_new_webroot(self, domain, allowraise=False): + def _prompt_for_new_webroot(self, domain, allowraise=False): # pylint: no-self-use code, webroot = ops.validated_directory( _validate_webroot, "Input the webroot for {0}:".format(domain), @@ -170,6 +169,10 @@ to serve all files under specified web root ({0}).""" # We ignore the last prefix in the next iteration, # as it does not correspond to a folder path ('/' or 'C:') for prefix in sorted(util.get_prefixes(self.full_roots[name])[:-1], key=len): + if os.path.isdir(prefix): + # Don't try to create directory if it already exists, as some filesystems + # won't reliably raise EEXIST or EISDIR if directory exists. + continue try: # Set owner as parent directory if possible, apply mode for Linux/Windows. # For Linux, this is coupled with the "umask" call above because @@ -184,14 +187,13 @@ to serve all files under specified web root ({0}).""" logger.info("Unable to change owner and uid of webroot directory") logger.debug("Error was: %s", exception) except OSError as exception: - if exception.errno not in (errno.EEXIST, errno.EISDIR): - raise errors.PluginError( - "Couldn't create root for {0} http-01 " - "challenge responses: {1}".format(name, exception)) + raise errors.PluginError( + "Couldn't create root for {0} http-01 " + "challenge responses: {1}".format(name, exception)) finally: os.umask(old_umask) - def _get_validation_path(self, root_path, achall): + def _get_validation_path(self, root_path, achall): # pylint: no-self-use return os.path.join(root_path, achall.chall.encode("token")) def _perform_single(self, achall):