diff --git a/letsencrypt-apache/letsencrypt_apache/centos-options-ssl-apache.conf b/letsencrypt-apache/letsencrypt_apache/centos-options-ssl-apache.conf new file mode 100644 index 000000000..fbe8da0f2 --- /dev/null +++ b/letsencrypt-apache/letsencrypt_apache/centos-options-ssl-apache.conf @@ -0,0 +1,21 @@ +# Baseline setting to Include for SSL sites + +SSLEngine on + +# Intermediate configuration, tweak to your needs +SSLProtocol all -SSLv2 -SSLv3 +SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA +SSLHonorCipherOrder on + +SSLOptions +StrictRequire + +# Add vhost name to log entries: +LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" vhost_combined +LogFormat "%v %h %l %u %t \"%r\" %>s %b" vhost_common + +#CustomLog /var/log/apache2/access.log vhost_combined +#LogLevel warn +#ErrorLog /var/log/apache2/error.log + +# Always ensure Cookies have "Secure" set (JAH 2012/1) +#Header edit Set-Cookie (?i)^(.*)(;\s*secure)??((\s*;)?(.*)) "$1; Secure$3$4" diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index 2d822b3a1..5ca2ddcb6 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -155,7 +155,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Set Version if self.version is None: self.version = self.get_version() - if self.version < (2, 4): + if self.version < (2, 2): raise errors.NotSupportedError( "Apache Version %s not supported.", str(self.version)) @@ -305,6 +305,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if not vhost.ssl: vhost = self.make_vhost_ssl(vhost) + self._add_servername_alias(target_name, vhost) self.assoc[target_name] = vhost return vhost @@ -335,6 +336,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): raise errors.PluginError( "VirtualHost not able to be selected.") + self._add_servername_alias(target_name, vhost) self.assoc[target_name] = vhost return vhost @@ -353,7 +355,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Points 1 - Address name with no SSL best_candidate = None best_points = 0 - for vhost in self.vhosts: if vhost.modmacro is True: continue @@ -692,7 +693,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Reload augeas to take into account the new vhost self.aug.load() - # Get Vhost augeas path for new vhost vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" % (ssl_fp, parser.case_i("VirtualHost"))) @@ -709,6 +709,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Add directives self._add_dummy_ssl_directives(vh_p) + self.save() # Log actions and create save notes logger.info("Created an SSL vhost at %s", ssl_fp) @@ -859,6 +860,22 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "insert_key_file_path") self.parser.add_dir(vh_path, "Include", self.mod_ssl_conf) + def _add_servername_alias(self, target_name, vhost): + fp = vhost.filep + vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" % + (fp, parser.case_i("VirtualHost"))) + if not vh_p: + return + vh_path = vh_p[0] + if (self.parser.find_dir("ServerName", target_name, start=vh_path, exclude=False) + or self.parser.find_dir("ServerAlias", target_name, start=vh_path, exclude=False)): + return + if not self.parser.find_dir("ServerName", None, start=vh_path, exclude=False): + self.parser.add_dir(vh_path, "ServerName", target_name) + else: + self.parser.add_dir(vh_path, "ServerAlias", target_name) + self._add_servernames(vhost) + def _add_name_vhost_if_necessary(self, vhost): """Add NameVirtualHost Directives if necessary for new vhost. @@ -874,9 +891,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # See if the exact address appears in any other vhost # Remember 1.1.1.1:* == 1.1.1.1 -> hence any() for addr in vhost.addrs: + # In Apache 2.2, when a NameVirtualHost directive is not + # set, "*" and "_default_" will conflict when sharing a port + if addr.get_addr() in ("*", "_default_"): + addrs = [obj.Addr((a, addr.get_port(),)) + for a in ("*", "_default_")] + for test_vh in self.vhosts: if (vhost.filep != test_vh.filep and - any(test_addr == addr for + any(test_addr in addrs for test_addr in test_vh.addrs) and not self.is_name_vhost(addr)): self.add_name_vhost(addr) @@ -1587,4 +1610,4 @@ def install_ssl_options_conf(options_ssl): # Check to make sure options-ssl.conf is installed if not os.path.isfile(options_ssl): - shutil.copyfile(constants.MOD_SSL_CONF_SRC, options_ssl) + shutil.copyfile(constants.os_constant("MOD_SSL_CONF_SRC"), options_ssl) diff --git a/letsencrypt-apache/letsencrypt_apache/constants.py b/letsencrypt-apache/letsencrypt_apache/constants.py index fe5ef3335..50156444b 100644 --- a/letsencrypt-apache/letsencrypt_apache/constants.py +++ b/letsencrypt-apache/letsencrypt_apache/constants.py @@ -16,7 +16,9 @@ CLI_DEFAULTS_DEBIAN = dict( le_vhost_ext="-le-ssl.conf", handle_mods=True, handle_sites=True, - challenge_location="/etc/apache2" + challenge_location="/etc/apache2", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "letsencrypt_apache", "options-ssl-apache.conf") ) CLI_DEFAULTS_CENTOS = dict( server_root="/etc/httpd", @@ -31,7 +33,9 @@ CLI_DEFAULTS_CENTOS = dict( le_vhost_ext="-le-ssl.conf", handle_mods=False, handle_sites=False, - challenge_location="/etc/httpd/conf.d" + challenge_location="/etc/httpd/conf.d", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "letsencrypt_apache", "centos-options-ssl-apache.conf") ) CLI_DEFAULTS_GENTOO = dict( server_root="/etc/apache2", @@ -46,7 +50,9 @@ CLI_DEFAULTS_GENTOO = dict( le_vhost_ext="-le-ssl.conf", handle_mods=False, handle_sites=False, - challenge_location="/etc/apache2/vhosts.d" + challenge_location="/etc/apache2/vhosts.d", + MOD_SSL_CONF_SRC=pkg_resources.resource_filename( + "letsencrypt_apache", "options-ssl-apache.conf") ) CLI_DEFAULTS = { "debian": CLI_DEFAULTS_DEBIAN, @@ -62,11 +68,6 @@ CLI_DEFAULTS = { MOD_SSL_CONF_DEST = "options-ssl-apache.conf" """Name of the mod_ssl config file as saved in `IConfig.config_dir`.""" -MOD_SSL_CONF_SRC = pkg_resources.resource_filename( - "letsencrypt_apache", "options-ssl-apache.conf") -"""Path to the Apache mod_ssl config file found in the Let's Encrypt -distribution.""" - AUGEAS_LENS_DIR = pkg_resources.resource_filename( "letsencrypt_apache", "augeas_lens") """Path to the Augeas lens directory""" diff --git a/letsencrypt-apache/letsencrypt_apache/parser.py b/letsencrypt-apache/letsencrypt_apache/parser.py index cc7f2ec42..3c13aae5f 100644 --- a/letsencrypt-apache/letsencrypt_apache/parser.py +++ b/letsencrypt-apache/letsencrypt_apache/parser.py @@ -597,7 +597,7 @@ class ApacheParser(object): .. todo:: Make sure that files are included """ - default = self._set_user_config_file() + default = self.loc["root"] temp = os.path.join(self.root, "ports.conf") if os.path.isfile(temp): @@ -618,23 +618,6 @@ class ApacheParser(object): raise errors.NoInstallationError("Could not find configuration root") - def _set_user_config_file(self): - """Set the appropriate user configuration file - - .. todo:: This will have to be updated for other distros versions - - :param str root: pathname which contains the user config - - """ - # Basic check to see if httpd.conf exists and - # in hierarchy via direct include - # httpd.conf was very common as a user file in Apache 2.2 - if (os.path.isfile(os.path.join(self.root, "httpd.conf")) and - self.find_dir("Include", "httpd.conf", self.loc["root"])): - return os.path.join(self.root, "httpd.conf") - else: - return os.path.join(self.root, "apache2.conf") - def case_i(string): """Returns case insensitive regex. diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py index 00a98e33a..a04f7904c 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py @@ -161,6 +161,7 @@ class TwoVhost80Test(util.ApacheTest): def test_choose_vhost_select_vhost_non_ssl(self, mock_select): mock_select.return_value = self.vh_truth[0] chosen_vhost = self.config.choose_vhost("none.com") + self.vh_truth[0].aliases.add("none.com") self.assertEqual( self.vh_truth[0].get_names(), chosen_vhost.get_names()) @@ -192,8 +193,8 @@ class TwoVhost80Test(util.ApacheTest): self.assertEqual( self.vh_truth[0], self.config._find_best_vhost("encryption-example.demo")) - self.assertTrue( - self.config._find_best_vhost("does-not-exist.com") is None) + self.assertEqual( + self.config._find_best_vhost("does-not-exist.com"), None) def test_find_best_vhost_variety(self): # pylint: disable=protected-access @@ -606,6 +607,14 @@ class TwoVhost80Test(util.ApacheTest): self.config._add_name_vhost_if_necessary(self.vh_truth[0]) self.assertTrue(self.config.save.called) + new_addrs = set() + for addr in self.vh_truth[0].addrs: + new_addrs.add(obj.Addr(("_default_", addr.get_port(),))) + + self.vh_truth[0].addrs = new_addrs + self.config._add_name_vhost_if_necessary(self.vh_truth[0]) + self.assertEqual(self.config.save.call_count, 2) + @mock.patch("letsencrypt_apache.configurator.tls_sni_01.ApacheTlsSni01.perform") @mock.patch("letsencrypt_apache.configurator.ApacheConfigurator.restart") def test_perform(self, mock_restart, mock_perform): diff --git a/letsencrypt-apache/letsencrypt_apache/tests/parser_test.py b/letsencrypt-apache/letsencrypt_apache/tests/parser_test.py index e976bc9f6..2e6481aba 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/parser_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/parser_test.py @@ -106,7 +106,7 @@ class BasicParserTest(util.ParserTest): def test_set_locations(self): with mock.patch("letsencrypt_apache.parser.os.path") as mock_path: - mock_path.isfile.side_effect = [True, False, False] + mock_path.isfile.side_effect = [False, False] # pylint: disable=protected-access results = self.parser._set_locations() @@ -114,16 +114,6 @@ class BasicParserTest(util.ParserTest): self.assertEqual(results["default"], results["listen"]) self.assertEqual(results["default"], results["name"]) - def test_set_user_config_file(self): - # pylint: disable=protected-access - path = os.path.join(self.parser.root, "httpd.conf") - open(path, 'w').close() - self.parser.add_dir(self.parser.loc["default"], "Include", - "httpd.conf") - - self.assertEqual( - path, self.parser._set_user_config_file()) - @mock.patch("letsencrypt_apache.parser.ApacheParser._get_runtime_cfg") def test_update_runtime_variables(self, mock_cfg): mock_cfg.return_value = ( diff --git a/letsencrypt-apache/letsencrypt_apache/tests/util.py b/letsencrypt-apache/letsencrypt_apache/tests/util.py index fb86d2320..97a11e851 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/util.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/util.py @@ -33,7 +33,7 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods pkg="letsencrypt_apache.tests") self.ssl_options = common.setup_ssl_options( - self.config_dir, constants.MOD_SSL_CONF_SRC, + self.config_dir, constants.os_constant("MOD_SSL_CONF_SRC"), constants.MOD_SSL_CONF_DEST) self.config_path = os.path.join(self.temp_dir, config_root) @@ -150,7 +150,7 @@ def get_vh_truth(temp_dir, config_name): os.path.join(prefix, "default-ssl-port-only.conf"), os.path.join(aug_pre, ("default-ssl-port-only.conf/" "IfModule/VirtualHost")), - set([obj.Addr.fromstring("_default_:443")]), True, False), + set([obj.Addr.fromstring("_default_:443")]), True, False) ] return vh_truth diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index e0812501c..7800a5eb6 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN=${VENV_PATH}/bin -LE_AUTO_VERSION="0.3.0" +LE_AUTO_VERSION="0.4.0.dev0" # This script takes the same arguments as the main letsencrypt program, but it # additionally responds to --verbose (more output) and --debug (allow support @@ -374,7 +374,7 @@ Bootstrap() { elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon - elif `grep -q openSUSE /etc/os-release` ; then + elif [ -f /etc/os-release] && `grep -q openSUSE /etc/os-release` ; then echo "Bootstrapping dependencies for openSUSE-based OSes..." BootstrapSuseCommon elif [ -f /etc/arch-release ]; then diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 8118a5f69..ba17d7a99 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -130,7 +130,7 @@ Bootstrap() { elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon - elif `grep -q openSUSE /etc/os-release` ; then + elif [ -f /etc/os-release] && `grep -q openSUSE /etc/os-release` ; then echo "Bootstrapping dependencies for openSUSE-based OSes..." BootstrapSuseCommon elif [ -f /etc/arch-release ]; then diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index 691688969..3d48d100f 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -310,10 +310,10 @@ class NginxConfigurator(common.Plugin): key = OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, le_key.pem) cert = acme_crypto_util.gen_ss_cert(key, domains=[socket.gethostname()]) - cert_path = os.path.join(tmp_dir, "cert.pem") cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, cert) - with open(cert_path, 'w') as cert_file: + cert_file, cert_path = le_util.unique_file(os.path.join(tmp_dir, "cert.pem")) + with cert_file: cert_file.write(cert_pem) return cert_path, le_key.file diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 6d55e9674..f762076d4 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -112,11 +112,15 @@ def usage_strings(plugins): return USAGE % (apache_doc, nginx_doc), SHORT_USAGE -def _find_domains(args, installer): - if not args.domains: +def _find_domains(config, installer): + if not config.domains: domains = display_ops.choose_names(installer) + # record in config.domains (so that it can be serialised in renewal config files), + # and set webroot_map entries if applicable + for d in domains: + _process_domain(config, d) else: - domains = args.domains + domains = config.domains if not domains: raise errors.Error("Please specify --domains, or --installer that " @@ -608,7 +612,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo except errors.PluginSelectionError, e: return e.message - domains = _find_domains(args, installer) + domains = _find_domains(config, installer) # TODO: Handle errors from _init_le_client? le_client = _init_le_client(args, config, authenticator, installer) @@ -630,7 +634,8 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo def obtain_cert(args, config, plugins): - """Authenticate & obtain cert, but do not install it.""" + """Implements "certonly": authenticate & obtain cert, but do not install it.""" + if args.domains and args.csr is not None: # TODO: --csr could have a priority, when --domains is # supplied, check if CSR matches given domains? @@ -657,7 +662,7 @@ def obtain_cert(args, config, plugins): certr, chain, args.cert_path, args.chain_path, args.fullchain_path) _report_new_cert(cert_path, cert_fullchain) else: - domains = _find_domains(args, installer) + domains = _find_domains(config, installer) _auth_from_domains(le_client, config, domains) if args.dry_run: @@ -677,7 +682,7 @@ def install(args, config, plugins): except errors.PluginSelectionError, e: return e.message - domains = _find_domains(args, installer) + domains = _find_domains(config, installer) le_client = _init_le_client( args, config, authenticator=None, installer=installer) assert args.cert_path is not None # required=True in the subparser @@ -851,6 +856,13 @@ class HelpfulArgumentParser(object): parsed_args.verb = self.verb # Do any post-parsing homework here + + # we get domains from -d, but also from the webroot map... + if parsed_args.webroot_map: + for domain in parsed_args.webroot_map.keys(): + if domain not in parsed_args.domains: + parsed_args.domains.append(domain) + if parsed_args.staging or parsed_args.dry_run: if (parsed_args.server not in (flag_default("server"), constants.STAGING_URI)): @@ -1286,11 +1298,12 @@ def _plugins_parsing(helpful, plugins): "handle different domains; each domain will have the webroot path that" " preceded it. For instance: `-w /var/www/example -d example.com -d " "www.example.com -w /var/www/thing -d thing.net -d m.thing.net`") - parse_dict = lambda s: dict(json.loads(s)) # --webroot-map still has some awkward properties, so it is undocumented - helpful.add("webroot", "--webroot-map", default={}, type=parse_dict, - help=argparse.SUPPRESS) - + helpful.add("webroot", "--webroot-map", default={}, action=WebrootMapProcessor, + help="JSON dictionary mapping domains to webroot paths; this implies -d " + "for each entry. You may need to escape this from your shell. " + """Eg: --webroot-map '{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}' """ + "This option is merged with, but takes precedence over, -w / -d entries") class WebrootPathProcessor(argparse.Action): # pylint: disable=missing-docstring def __init__(self, *args, **kwargs): @@ -1319,18 +1332,36 @@ class WebrootPathProcessor(argparse.Action): # pylint: disable=missing-docstring config.webroot_path.append(webroot) +_undot = lambda domain: domain[:-1] if domain.endswith('.') else domain + +def _process_domain(config, domain_arg, webroot_path=None): + """ + Process a new -d flag, helping the webroot plugin construct a map of + {domain : webrootpath} if -w / --webroot-path is in use + """ + webroot_path = webroot_path if webroot_path else config.webroot_path + + for domain in (d.strip() for d in domain_arg.split(",")): + if domain not in config.domains: + domain = _undot(domain) + config.domains.append(domain) + # Each domain has a webroot_path of the most recent -w flag + # unless it was explicitly included in webroot_map + if webroot_path: + config.webroot_map.setdefault(domain, webroot_path[-1]) + + +class WebrootMapProcessor(argparse.Action): # pylint: disable=missing-docstring + def __call__(self, parser, config, webroot_map_arg, option_string=None): + webroot_map = json.loads(webroot_map_arg) + for domains, webroot_path in webroot_map.iteritems(): + _process_domain(config, domains, [webroot_path]) + + class DomainFlagProcessor(argparse.Action): # pylint: disable=missing-docstring def __call__(self, parser, config, domain_arg, option_string=None): - """ - Process a new -d flag, helping the webroot plugin construct a map of - {domain : webrootpath} if -w / --webroot-path is in use - """ - for domain in (d.strip() for d in domain_arg.split(",")): - if domain not in config.domains: - config.domains.append(domain) - # Each domain has a webroot_path of the most recent -w flag - if config.webroot_path: - config.webroot_map[domain] = config.webroot_path[-1] + """Just wrap _process_domain in argparseese.""" + _process_domain(config, domain_arg) def setup_log_file_handler(args, logfile, fmt): diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 730c32398..76265a739 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -53,8 +53,8 @@ def init_save_key(key_size, key_dir, keyname="key-letsencrypt.pem"): config.strict_permissions) key_f, key_path = le_util.unique_file( os.path.join(key_dir, keyname), 0o600) - key_f.write(key_pem) - key_f.close() + with key_f: + key_f.write(key_pem) logger.info("Generating key (%d bits): %s", key_size, key_path) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 64295a80f..d97d43dc6 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -298,18 +298,19 @@ def check_domain_sanity(domain): # Check if there's a wildcard domain if domain.startswith("*."): raise errors.ConfigurationError( - "Wildcard domains are not supported") + "Wildcard domains are not supported: {0}".format(domain)) # Punycode if "xn--" in domain: raise errors.ConfigurationError( - "Punycode domains are not presently supported") + "Punycode domains are not presently supported: {0}".format(domain)) # Unicode try: domain.encode('ascii') except UnicodeDecodeError: raise errors.ConfigurationError( - "Internationalized domain names are not presently supported") + "Internationalized domain names are not presently supported: {0}" + .format(domain)) # FQDN checks from # http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/ @@ -317,4 +318,4 @@ def check_domain_sanity(domain): # first and last char is not "-" fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(?