Repository: certbot/certbot

Issue:
Domain with german umlauts ('ascii' codec can't encode character)
CentOS 7.1.1503

I tried to create an certificat with domain contains german umlauts. Domain is: entä.de
I getting in the console this error:

```
An unexpected error occurred:
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe4' in position 3: ordinal not in range(128)
Please see the logfiles in /var/log/letsencrypt for more details.
```

Logfile:

``` python
2015-12-08 17:58:44,353:DEBUG:letsencrypt.cli:Exiting abnormally:
Traceback (most recent call last):
  File "/root/.local/share/letsencrypt/bin/letsencrypt", line 11, in <module>
    sys.exit(main())
  File "/root/.local/share/letsencrypt/lib/python2.7/site-packages/letsencrypt/cli.py", line 1283, in main
    return args.func(args, config, plugins)
  File "/root/.local/share/letsencrypt/lib/python2.7/site-packages/letsencrypt/cli.py", line 511, in obtain_cert
    domains = _find_domains(args, installer)
  File "/root/.local/share/letsencrypt/lib/python2.7/site-packages/letsencrypt/cli.py", line 106, in _find_domains
    domains = display_ops.choose_names(installer)
  File "/root/.local/share/letsencrypt/lib/python2.7/site-packages/letsencrypt/display/ops.py", line 187, in choose_names
    return _choose_names_manually()
  File "/root/.local/share/letsencrypt/lib/python2.7/site-packages/letsencrypt/display/ops.py", line 235, in _choose_names_manually
    return display_util.separate_list_input(input_)
  File "/root/.local/share/letsencrypt/lib/python2.7/site-packages/letsencrypt/display/util.py", line 419, in separate_list_input
    return [str(string) for string in no_commas.split()]
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe4' in position 3: ordinal not in range(128)
```

Is there a way to fix this?



Discussion:
Displaying this error message is a bug. Nevertheless I don't think you can use IDN

Yeah, IDNs are not currently supported. I just did work on the domain validation, and can use the same checks here. Fixing this in a minute.

_edit:  maybe not in a minute, but tomorrow. Going to display correct error messages and let the user re-enter the values perhaps._


Resolving patch:
diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py
--- a/letsencrypt/display/ops.py
+++ b/letsencrypt/display/ops.py
@@ -123,6 +123,7 @@ def pick_configurator(
         config, default, plugins, question,
         (interfaces.IAuthenticator, interfaces.IInstaller))
 
+
 def get_email(more=False, invalid=False):
     """Prompt for valid email address.
 
@@ -209,6 +210,7 @@ def choose_names(installer):
     else:
         return []
 
+
 def get_valid_domains(domains):
     """Helper method for choose_names that implements basic checks
      on domain names
@@ -226,6 +228,7 @@ def get_valid_domains(domains):
             continue
     return valid_domains
 
+
 def _filter_names(names):
     """Determine which names the user would like to select from a list.
 
@@ -250,7 +253,41 @@ def _choose_names_manually():
         "Please enter in your domain name(s) (comma and/or space separated) ")
 
     if code == display_util.OK:
-        return display_util.separate_list_input(input_)
+        invalid_domains = dict()
+        retry_message = ""
+        try:
+            domain_list = display_util.separate_list_input(input_)
+        except UnicodeEncodeError:
+            domain_list = []
+            retry_message = (
+                "Internationalized domain names are not presently "
+                "supported.{0}{0}Would you like to re-enter the "
+                "names?{0}").format(os.linesep)
+
+        for domain in domain_list:
+            try:
+                le_util.check_domain_sanity(domain)
+            except errors.ConfigurationError as e:
+                invalid_domains[domain] = e.message
+
+        if len(invalid_domains):
+            retry_message = (
+                "One or more of the entered domain names was not valid:"
+                "{0}{0}").format(os.linesep)
+            for domain in invalid_domains:
+                retry_message = retry_message + "{1}: {2}{0}".format(
+                    os.linesep, domain, invalid_domains[domain])
+            retry_message = retry_message + (
+                "{0}Would you like to re-enter the names?{0}").format(
+                    os.linesep)
+
+        if retry_message:
+            # We had error in input
+            retry = util(interfaces.IDisplay).yesno(retry_message)
+            if retry:
+                return _choose_names_manually()
+        else:
+            return domain_list
     return []
 
 


Test patch:
diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py
--- a/letsencrypt/tests/display/ops_test.py
+++ b/letsencrypt/tests/display/ops_test.py
@@ -397,6 +397,44 @@ def test_get_valid_domains(self):
         self.assertEqual(get_valid_domains(all_invalid), [])
         self.assertEqual(len(get_valid_domains(two_valid)), 2)
 
+    @mock.patch("letsencrypt.display.ops.util")
+    def test_choose_manually(self, mock_util):
+        from letsencrypt.display.ops import _choose_names_manually
+        # No retry
+        mock_util().yesno.return_value = False
+        # IDN and no retry
+        mock_util().input.return_value = (display_util.OK,
+                                          "uniçodé.com")
+        self.assertEqual(_choose_names_manually(), [])
+        # IDN exception with previous mocks
+        with mock.patch("letsencrypt.display.util") as mock_sl:
+            uerror = UnicodeEncodeError('mock', u'',
+                                        0, 1, 'mock')
+            mock_sl.separate_list_input.side_effect = uerror
+            self.assertEqual(_choose_names_manually(), [])
+        # Punycode and no retry
+        mock_util().input.return_value = (display_util.OK,
+                                          "xn--ls8h.tld")
+        self.assertEqual(_choose_names_manually(), [])
+        # non-FQDN and no retry
+        mock_util().input.return_value = (display_util.OK,
+                                          "notFQDN")
+        self.assertEqual(_choose_names_manually(), [])
+        # Two valid domains
+        mock_util().input.return_value = (display_util.OK,
+                                          ("example.com,"
+                                           "valid.example.com"))
+        self.assertEqual(_choose_names_manually(),
+                         ["example.com", "valid.example.com"])
+        # Three iterations
+        mock_util().input.return_value = (display_util.OK,
+                                          "notFQDN")
+        yn = mock.MagicMock()
+        yn.side_effect = [True, True, False]
+        mock_util().yesno = yn
+        _choose_names_manually()
+        self.assertEqual(mock_util().yesno.call_count, 3)
+
 
 class SuccessInstallationTest(unittest.TestCase):
     # pylint: disable=too-few-public-methods