Repository: certbot/certbot

Issue:
Webroot Unicode Issue
**Certbot Version: 0.31.0-1+ubuntu16.04.1+certbot+1
Ubuntu 16.04.6 LTS
Installation from certbot PPA.**

**CLI Command (Real domain name changed):**
letsencrypt --agree-tos --noninteractive --webroot certonly --webroot-path /data/www/k000009/web/hüüüü.at --cert-name www.xn--h-ehaaaa.at --domains www.xn--h-ehaaaa.at,xn--h-ehaaaa.at --force-renew

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator webroot, Installer None
Obtaining a new certificate
An unexpected error occurred:
UnicodeEncodeError: 'ascii' codec can't encode characters in position 659-662: ordinal not in range(128)
Please see the logfiles in /var/log/letsencrypt for more details.

**Log File Output:**
2019-11-12 14:33:00,609:DEBUG:certbot.plugins.selection:Requested authenticator webroot and installer <certbot.cli._Default object at 0x7fcbae54d080>
2019-11-12 14:33:00,614:DEBUG:certbot.cli:Var authenticator=webroot (set by user).
2019-11-12 14:33:00,617:DEBUG:certbot.cli:Var webroot_path=/data/www/k000009/web/hüüüü.at (set by user).
2019-11-12 14:33:00,622:DEBUG:certbot.cli:Var webroot_path=/data/www/k000009/web/hüüüü.at (set by user).
2019-11-12 14:33:00,623:DEBUG:certbot.cli:Var webroot_map={'webroot_path'} (set by user).
2019-11-12 14:33:00,633:DEBUG:certbot.storage:Writing new config /etc/letsencrypt/renewal/www.letest.xn--h-ehaaaa.at-0003.conf.
2019-11-12 14:33:00,634:DEBUG:certbot.log:Exiting abnormally:
Traceback (most recent call last):
  File "/usr/bin/letsencrypt", line 11, in <module>
    load_entry_point('certbot==0.31.0', 'console_scripts', 'certbot')()
  File "/usr/lib/python3/dist-packages/certbot/main.py", line 1365, in main
    return config.func(config, plugins)
  File "/usr/lib/python3/dist-packages/certbot/main.py", line 1250, in certonly
    lineage = _get_and_save_cert(le_client, config, domains, certname, lineage)
  File "/usr/lib/python3/dist-packages/certbot/main.py", line 121, in _get_and_save_cert
    lineage = le_client.obtain_and_enroll_certificate(domains, certname)
  File "/usr/lib/python3/dist-packages/certbot/client.py", line 428, in obtain_and_enroll_certificate
    self.config)
  File "/usr/lib/python3/dist-packages/certbot/storage.py", line 1048, in new_lineage
    target, values)
  File "/usr/lib/python3/dist-packages/certbot/storage.py", line 150, in write_renewal_config
    config.write(outfile=f)
  File "/usr/lib/python3/dist-packages/configobj.py", line 2119, in write
    'ascii')
UnicodeEncodeError: 'ascii' codec can't encode characters in position 659-662: ordinal not in range(128)

**Expected behaviour:**
The current certbot client crashes, if you use a webroot with unicode characters e.g. "üäö". The client should allow unicode characters, which are widely supported by operating systems.


Discussion:
Hello!

Thanks for your report. I confirm the bug. Steps to reproduce it:

```
# Assuming to be in the certbot GIT repository root
python tools/venv.py
source venv/bin/activate
rm -rf .certbot_test_workspace
mkdir /tmp/üüü.h
run_acme_server &
cd /tmp/üüü.h
sudo python -m SimpleHTTPServer &
certbot_test certonly --webroot --webroot-path /tmp/üüü.h -d test.example.net
```

Resolving patch:
diff --git a/certbot/certbot/_internal/main.py b/certbot/certbot/_internal/main.py
--- a/certbot/certbot/_internal/main.py
+++ b/certbot/certbot/_internal/main.py
@@ -613,7 +613,9 @@ def _delete_if_appropriate(config):
 
     # don't delete if the archive_dir is used by some other lineage
     archive_dir = storage.full_archive_path(
-            configobj.ConfigObj(storage.renewal_file_for_certname(config, config.certname)),
+            configobj.ConfigObj(
+                storage.renewal_file_for_certname(config, config.certname),
+                encoding='utf-8', default_encoding='utf-8'),
             config, config.certname)
     try:
         cert_manager.match_and_check_overlaps(config, [lambda x: archive_dir],
diff --git a/certbot/certbot/_internal/storage.py b/certbot/certbot/_internal/storage.py
--- a/certbot/certbot/_internal/storage.py
+++ b/certbot/certbot/_internal/storage.py
@@ -67,13 +67,16 @@ def cert_path_for_cert_name(config: interfaces.IConfig, cert_name: str) -> str:
 
     """
     cert_name_implied_conf = renewal_file_for_certname(config, cert_name)
-    return configobj.ConfigObj(cert_name_implied_conf)["fullchain"]
+    return configobj.ConfigObj(
+        cert_name_implied_conf, encoding='utf-8', default_encoding='utf-8')["fullchain"]
 
 
 def config_with_defaults(config=None):
     """Merge supplied config, if provided, on top of builtin defaults."""
-    defaults_copy = configobj.ConfigObj(constants.RENEWER_DEFAULTS)
-    defaults_copy.merge(config if config is not None else configobj.ConfigObj())
+    defaults_copy = configobj.ConfigObj(
+        constants.RENEWER_DEFAULTS, encoding='utf-8', default_encoding='utf-8')
+    defaults_copy.merge(config if config is not None else configobj.ConfigObj(
+        encoding='utf-8', default_encoding='utf-8'))
     return defaults_copy
 
 
@@ -114,7 +117,7 @@ def write_renewal_config(o_filename, n_filename, archive_dir, target, relevant_d
     :rtype: configobj.ConfigObj
 
     """
-    config = configobj.ConfigObj(o_filename)
+    config = configobj.ConfigObj(o_filename, encoding='utf-8', default_encoding='utf-8')
     config["version"] = certbot.__version__
     config["archive_dir"] = archive_dir
     for kind in ALL_FOUR:
@@ -196,7 +199,7 @@ def update_configuration(lineagename, archive_dir, target, cli_config):
     write_renewal_config(config_filename, temp_filename, archive_dir, target, values)
     filesystem.replace(temp_filename, config_filename)
 
-    return configobj.ConfigObj(config_filename)
+    return configobj.ConfigObj(config_filename, encoding='utf-8', default_encoding='utf-8')
 
 
 def get_link_target(link):
@@ -324,7 +327,8 @@ def delete_files(config, certname):
     full_default_archive_dir = full_archive_path(None, config, certname)
     full_default_live_dir = _full_live_path(config, certname)
     try:
-        renewal_config = configobj.ConfigObj(renewal_filename)
+        renewal_config = configobj.ConfigObj(
+            renewal_filename, encoding='utf-8', default_encoding='utf-8')
     except configobj.ConfigObjError:
         # config is corrupted
         logger.warning("Could not parse %s. You may wish to manually "
@@ -434,7 +438,8 @@ def __init__(self, config_filename, cli_config, update_symlinks=False):
         # systemwide renewal configuration; self.configfile should be
         # used to make and save changes.
         try:
-            self.configfile = configobj.ConfigObj(config_filename)
+            self.configfile = configobj.ConfigObj(
+                config_filename, encoding='utf-8', default_encoding='utf-8')
         except configobj.ConfigObjError:
             raise errors.CertStorageError(
                 "error parsing {0}".format(config_filename))


Test patch: