From 94af2357130474199d9bd348a8be57a326d17509 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sat, 23 Oct 2021 23:37:40 +0200 Subject: [PATCH] Generate a web.config file for IIS to serve properly the challenge files in webroot plugin (#9054) * Generate a web.config file to serve properly challenge files with IIS * Fix cleanup, add test * FIx lint * Do not overwrite existing web.config. Delete only web.config when it has been created by Certbot and is unmodified. * Fix lint * Update certbot/certbot/_internal/plugins/webroot.py Co-authored-by: alexzorin * Add log * Check for POSIX_MODE before web.config deletion attempt. * Add documentation * Update certbot/CHANGELOG.md Co-authored-by: alexzorin * Update certbot/docs/using.rst Co-authored-by: alexzorin --- certbot/CHANGELOG.md | 4 +- certbot/certbot/_internal/plugins/webroot.py | 44 ++++++++++++++++++++ certbot/docs/using.rst | 4 ++ certbot/tests/plugins/webroot_test.py | 29 +++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 69dc00628..e227fe652 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -6,7 +6,9 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). ### Added -* +* Certbot will generate a `web.config` file on Windows in the challenge path + when the `webroot` plugin is used, if one does not exist. This `web.config` file + lets IIS serve challenge files while they do not have an extension. ### Changed diff --git a/certbot/certbot/_internal/plugins/webroot.py b/certbot/certbot/_internal/plugins/webroot.py index 8d8cf0e45..a12741d1a 100644 --- a/certbot/certbot/_internal/plugins/webroot.py +++ b/certbot/certbot/_internal/plugins/webroot.py @@ -9,6 +9,7 @@ from typing import List from typing import Set from acme import challenges +from certbot import crypto_util from certbot import errors from certbot import interfaces from certbot._internal import cli @@ -24,6 +25,23 @@ from certbot.util import safe_open logger = logging.getLogger(__name__) +_WEB_CONFIG_CONTENT = """\ + + + + + + + + + +""" +# This list references the hashes of all versions of the web.config files that Certbot could +# have generated during an HTTP-01 challenge. If you modify _WEB_CONFIG_CONTENT, you MUST add +# the new hash in this list. +_WEB_CONFIG_SHA256SUMS = ["20c5ca1bd58fa8ad5f07a2f1be8b7cbb707c20fcb607a8fc8db9393952846a97"] + + class Authenticator(common.Plugin, interfaces.Authenticator): """Webroot Authenticator.""" @@ -192,6 +210,19 @@ to serve all files under specified web root ({0}).""" finally: filesystem.umask(old_umask) + # On Windows, generate a local web.config file that allows IIS to serve expose + # challenge files despite the fact they do not have a file extension. + if not filesystem.POSIX_MODE: + web_config_path = os.path.join(self.full_roots[name], "web.config") + if os.path.exists(web_config_path): + logger.info("A web.config file has not been created in " + "%s because another one already exists.", self.full_roots[name]) + return + logger.info("Creating a web.config file in %s to allow IIS " + "to serve challenge files.", self.full_roots[name]) + with safe_open(web_config_path, mode="w", chmod=0o644) as web_config: + web_config.write(_WEB_CONFIG_CONTENT) + def _get_validation_path(self, root_path, achall): return os.path.join(root_path, achall.chall.encode("token")) @@ -223,6 +254,19 @@ to serve all files under specified web root ({0}).""" os.remove(validation_path) self.performed[root_path].remove(achall) + if not filesystem.POSIX_MODE: + web_config_path = os.path.join(root_path, "web.config") + if os.path.exists(web_config_path): + sha256sum = crypto_util.sha256sum(web_config_path) + if sha256sum in _WEB_CONFIG_SHA256SUMS: + logger.info("Cleaning web.config file generated by Certbot in %s.", + root_path) + os.remove(web_config_path) + else: + logger.info("Not cleaning up the web.config file in %s " + "because it is not generated by Certbot.", root_path) + + not_removed: List[str] = [] while self._created_dirs: path = self._created_dirs.pop() diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst index bdb343996..1f441cc55 100644 --- a/certbot/docs/using.rst +++ b/certbot/docs/using.rst @@ -147,6 +147,10 @@ your webserver configuration, you might need to modify the configuration to ensure that files inside ``/.well-known/acme-challenge`` are served by the webserver. +Under Windows, Certbot will generate a ``web.config`` file, if one does not already exist, +in ``/.well-known/acme-challenge`` in order to let IIS serve the challenge files even if they +do not have an extension. + Nginx ----- diff --git a/certbot/tests/plugins/webroot_test.py b/certbot/tests/plugins/webroot_test.py index 53bea8218..54c51ae1d 100644 --- a/certbot/tests/plugins/webroot_test.py +++ b/certbot/tests/plugins/webroot_test.py @@ -86,6 +86,35 @@ class AuthenticatorTest(unittest.TestCase): self.assertEqual(self.config.webroot_map[self.achall.domain], self.path) + @unittest.skipIf(filesystem.POSIX_MODE, reason='Test specific to Windows') + @test_util.patch_display_util() + def test_webconfig_file_generate_and_cleanup(self, mock_get_utility): + mock_display = mock_get_utility() + mock_display.menu.return_value = (display_util.OK, 1,) + + self.auth.perform([self.achall]) + self.assertTrue(os.path.exists(os.path.join(self.root_challenge_path, "web.config"))) + self.auth.cleanup([self.achall]) + self.assertFalse(os.path.exists(os.path.join(self.root_challenge_path, "web.config"))) + + @unittest.skipIf(filesystem.POSIX_MODE, reason='Test specific to Windows') + @test_util.patch_display_util() + def test_foreign_webconfig_file_handling(self, mock_get_utility): + mock_display = mock_get_utility() + mock_display.menu.return_value = (display_util.OK, 1,) + + challenge_path = os.path.join(self.path, ".well-known", "acme-challenge") + filesystem.makedirs(challenge_path) + + webconfig_path = os.path.join(challenge_path, "web.config") + with open(webconfig_path, "w") as file: + file.write("something") + self.auth.perform([self.achall]) + from certbot import crypto_util + webconfig_hash = crypto_util.sha256sum(webconfig_path) + from certbot._internal.plugins.webroot import _WEB_CONFIG_SHA256SUMS + self.assertTrue(webconfig_hash not in _WEB_CONFIG_SHA256SUMS) + @test_util.patch_display_util() def test_webroot_from_list_help_and_cancel(self, mock_get_utility): self.config.webroot_path = []