Repository: certbot/certbot

Issue:
Don't try to auto-configure local names
In https://community.letsencrypt.org/t/dns-name-does-not-have-enough-labels/5036/2, we have a couple of people who ran into trouble because they had a virtualhost for a local name, i.e. one with no dots. We should omit these from the list of names we try to auto-configure.

More generally, we could try to reproduce some of the validity checks that happen in boulder, so if you have "foo.corp", the client could see that that doesn't end in a public suffix and ignore it.

Alternately, we could be a little friendlier about presenting "these names failed to validate. do you want to issue a cert for the other names anyhow, or keep trying to validate these names?"



Discussion:
I had  seen this with nginx and filed #1664, but perhaps it makes more sense to handle pruning of local names in the main client.


Resolving patch:
diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py
--- a/letsencrypt/configuration.py
+++ b/letsencrypt/configuration.py
@@ -1,13 +1,13 @@
 """Let's Encrypt user-supplied configuration."""
 import os
 import urlparse
-import re
 
 import zope.interface
 
 from letsencrypt import constants
 from letsencrypt import errors
 from letsencrypt import interfaces
+from letsencrypt import le_util
 
 
 class NamespaceConfig(object):
@@ -123,40 +123,5 @@ def check_config_sanity(config):
 
     # Domain checks
     if config.namespace.domains is not None:
-        _check_config_domain_sanity(config.namespace.domains)
-
-
-def _check_config_domain_sanity(domains):
-    """Helper method for check_config_sanity which validates
-    domain flag values and errors out if the requirements are not met.
-
-    :param domains: List of domains
-    :type domains: `list` of `string`
-    :raises ConfigurationError: for invalid domains and cases where Let's
-                                Encrypt currently will not issue certificates
-
-    """
-    # Check if there's a wildcard domain
-    if any(d.startswith("*.") for d in domains):
-        raise errors.ConfigurationError(
-            "Wildcard domains are not supported")
-    # Punycode
-    if any("xn--" in d for d in domains):
-        raise errors.ConfigurationError(
-            "Punycode domains are not supported")
-
-    # Unicode
-    try:
-        for domain in domains:
-            domain.encode('ascii')
-    except UnicodeDecodeError:
-        raise errors.ConfigurationError(
-            "Internationalized domain names are not supported")
-
-    # FQDN checks from
-    # http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/
-    #  Characters used, domain parts < 63 chars, tld > 1 < 64 chars
-    #  first and last char is not "-"
-    fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,63}$")
-    if any(True for d in domains if not fqdn.match(d)):
-        raise errors.ConfigurationError("Requested domain is not a FQDN")
+        for domain in config.namespace.domains:
+            le_util.check_domain_sanity(domain)
diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py
--- a/letsencrypt/display/ops.py
+++ b/letsencrypt/display/ops.py
@@ -4,6 +4,7 @@
 
 import zope.component
 
+from letsencrypt import errors
 from letsencrypt import interfaces
 from letsencrypt import le_util
 from letsencrypt.display import util as display_util
@@ -186,7 +187,8 @@ def choose_names(installer):
         logger.debug("No installer, picking names manually")
         return _choose_names_manually()
 
-    names = list(installer.get_all_names())
+    domains = list(installer.get_all_names())
+    names = get_valid_domains(domains)
 
     if not names:
         manual = util(interfaces.IDisplay).yesno(
@@ -207,6 +209,22 @@ def choose_names(installer):
     else:
         return []
 
+def get_valid_domains(domains):
+    """Helper method for choose_names that implements basic checks
+     on domain names
+
+    :param list domains: Domain names to validate
+    :return: List of valid domains
+    :rtype: list
+    """
+    valid_domains = []
+    for domain in domains:
+        try:
+            le_util.check_domain_sanity(domain)
+            valid_domains.append(domain)
+        except errors.ConfigurationError:
+            continue
+    return valid_domains
 
 def _filter_names(names):
     """Determine which names the user would like to select from a list.
diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py
--- a/letsencrypt/le_util.py
+++ b/letsencrypt/le_util.py
@@ -280,3 +280,37 @@ def __call__(self, unused1, unused2, unused3, option_string=None):
 
     add_argument(argument_name, action=ShowWarning,
                  help=argparse.SUPPRESS, nargs=nargs)
+
+def check_domain_sanity(domain):
+    """Method which validates domain value and errors out if
+    the requirements are not met.
+
+    :param domain: Domain to check
+    :type domains: `string`
+    :raises ConfigurationError: for invalid domains and cases where Let's
+                                Encrypt currently will not issue certificates
+
+    """
+    # Check if there's a wildcard domain
+    if domain.startswith("*."):
+        raise errors.ConfigurationError(
+            "Wildcard domains are not supported")
+    # Punycode
+    if "xn--" in domain:
+        raise errors.ConfigurationError(
+            "Punycode domains are not presently supported")
+
+    # Unicode
+    try:
+        domain.encode('ascii')
+    except UnicodeDecodeError:
+        raise errors.ConfigurationError(
+            "Internationalized domain names are not presently supported")
+
+    # FQDN checks from
+    # http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/
+    #  Characters used, domain parts < 63 chars, tld > 1 < 64 chars
+    #  first and last char is not "-"
+    fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,63}$")
+    if not fqdn.match(domain):
+        raise errors.ConfigurationError("Requested domain is not a FQDN")


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
@@ -1,3 +1,4 @@
+# coding=utf-8
 """Test letsencrypt.display.ops."""
 import os
 import sys
@@ -385,6 +386,17 @@ def test_filter_names_cancel(self, mock_util):
 
         self.assertEqual(self._call(self.mock_install), [])
 
+    def test_get_valid_domains(self):
+        from letsencrypt.display.ops import get_valid_domains
+        all_valid = ["example.com", "second.example.com",
+                     "also.example.com"]
+        all_invalid = ["xn--ls8h.tld", "*.wildcard.com", "notFQDN",
+                       "uniçodé.com"]
+        two_valid = ["example.com", "xn--ls8h.tld", "also.example.com"]
+        self.assertEqual(get_valid_domains(all_valid), all_valid)
+        self.assertEqual(get_valid_domains(all_invalid), [])
+        self.assertEqual(len(get_valid_domains(two_valid)), 2)
+
 
 class SuccessInstallationTest(unittest.TestCase):
     # pylint: disable=too-few-public-methods