Merge branch 'config_sanity'

This commit is contained in:
Peter Eckersley 2015-11-09 16:19:21 -08:00
commit 6c3ea0d2a1
5 changed files with 87 additions and 21 deletions

View file

@ -28,6 +28,7 @@ from letsencrypt import configuration
from letsencrypt import constants
from letsencrypt import client
from letsencrypt import crypto_util
from letsencrypt import errors
from letsencrypt import interfaces
from letsencrypt import le_util
from letsencrypt import log
@ -36,7 +37,6 @@ from letsencrypt import storage
from letsencrypt.display import util as display_util
from letsencrypt.display import ops as display_ops
from letsencrypt.errors import Error, PluginSelectionError, CertStorageError
from letsencrypt.plugins import disco as plugins_disco
@ -106,8 +106,8 @@ def _find_domains(args, installer):
domains = args.domains
if not domains:
raise Error("Please specify --domains, or --installer that "
"will help in domain names autodiscovery")
raise errors.Error("Please specify --domains, or --installer that "
"will help in domain names autodiscovery")
return domains
@ -159,9 +159,9 @@ def _determine_account(args, config):
try:
acc, acme = client.register(
config, account_storage, tos_cb=_tos_cb)
except Error as error:
except errors.Error as error:
logger.debug(error, exc_info=True)
raise Error(
raise errors.Error(
"Unable to register an account with ACME server")
args.account = acc.id
@ -195,7 +195,7 @@ def _find_duplicative_certs(config, domains):
try:
full_path = os.path.join(configs_dir, renewal_file)
candidate_lineage = storage.RenewableCert(full_path, cli_config)
except (CertStorageError, IOError):
except (errors.CertStorageError, IOError):
logger.warning("Renewal configuration file %s is broken. "
"Skipping.", full_path)
continue
@ -267,7 +267,7 @@ def _treat_as_renewal(config, domains):
br=os.linesep
),
reporter_util.HIGH_PRIORITY)
raise Error(
raise errors.Error(
"User did not use proper CLI and would like "
"to reinvoke the client.")
@ -327,7 +327,7 @@ def _auth_from_domains(le_client, config, domains, plugins):
# TREAT AS NEW REQUEST
lineage = le_client.obtain_and_enroll_certificate(domains, plugins)
if not lineage:
raise Error("Certificate could not be obtained")
raise errors.Error("Certificate could not be obtained")
_report_new_cert(lineage.cert, lineage.fullchain)
@ -346,7 +346,7 @@ def set_configurator(previously, now):
if previously:
if previously != now:
msg = "Too many flags setting configurators/installers/authenticators {0} -> {1}"
raise PluginSelectionError(msg.format(repr(previously), repr(now)))
raise errors.PluginSelectionError(msg.format(repr(previously), repr(now)))
return now
@ -379,7 +379,7 @@ def diagnose_configurator_problem(cfg_type, requested, plugins):
'"letsencrypt-auto certonly" to get a cert you can install manually')
else:
msg = "{0} could not be determined or is not installed".format(cfg_type)
raise PluginSelectionError(msg)
raise errors.PluginSelectionError(msg)
def choose_configurator_plugins(args, config, plugins, verb):
@ -439,7 +439,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
"""Obtain a certificate and install."""
try:
installer, authenticator = choose_configurator_plugins(args, config, plugins, "run")
except PluginSelectionError, e:
except errors.PluginSelectionError, e:
return e.message
domains = _find_domains(args, installer)
@ -472,7 +472,7 @@ def obtaincert(args, config, plugins):
try:
# installers are used in auth mode to determine domain names
installer, authenticator = choose_configurator_plugins(args, config, plugins, "certonly")
except PluginSelectionError, e:
except errors.PluginSelectionError, e:
return e.message
# TODO: Handle errors from _init_le_client?
@ -497,7 +497,7 @@ def install(args, config, plugins):
try:
installer, _ = choose_configurator_plugins(args, config,
plugins, "install")
except PluginSelectionError, e:
except errors.PluginSelectionError, e:
return e.message
domains = _find_domains(args, installer)
@ -1060,7 +1060,7 @@ def _handle_exception(exc_type, exc_value, trace, args):
sys.exit("".join(
traceback.format_exception(exc_type, exc_value, trace)))
if issubclass(exc_type, Error):
if issubclass(exc_type, errors.Error):
sys.exit(exc_value)
else:
# Tell the user a bit about what happened, without overwhelming
@ -1124,7 +1124,7 @@ def main(cli_args=sys.argv[1:]):
disclaimer = pkg_resources.resource_string("letsencrypt", "DISCLAIMER")
if not zope.component.getUtility(interfaces.IDisplay).yesno(
disclaimer, "Agree", "Cancel"):
raise Error("Must agree to TOS")
raise errors.Error("Must agree to TOS")
if not os.geteuid() == 0:
logger.warning(
@ -1139,7 +1139,6 @@ def main(cli_args=sys.argv[1:]):
return args.func(args, config, plugins)
if __name__ == "__main__":
err_string = main()
if err_string:

View file

@ -1,6 +1,7 @@
"""Let's Encrypt user-supplied configuration."""
import os
import urlparse
import re
import zope.interface
@ -36,11 +37,8 @@ class NamespaceConfig(object):
def __init__(self, namespace):
self.namespace = namespace
if self.http01_port == self.tls_sni_01_port:
raise errors.Error(
"Trying to run http-01 and tls-sni-01 "
"on the same port ({0})".format(self.tls_sni_01_port))
# Check command line parameters sanity, and error out in case of problem.
check_config_sanity(self)
def __getattr__(self, name):
return getattr(self.namespace, name)
@ -111,3 +109,49 @@ class RenewerConfiguration(object):
def renewer_config_file(self): # pylint: disable=missing-docstring
return os.path.join(
self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME)
def check_config_sanity(config):
"""Validate command line options and display error message if
requirements are not met.
:param config: IConfig instance holding user configuration
:type args: :class:`letsencrypt.interfaces.IConfig`
"""
# Port check
if config.http01_port == config.tls_sni_01_port:
raise errors.ConfigurationError(
"Trying to run http-01 and tls-sni-01 "
"on the same port ({0})".format(config.tls_sni_01_port))
# 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")
# FQDN checks from
# http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/
# Characters used, domain parts < 63 chars, tld > 1 < 7 chars
# first and last char is not "-"
fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}$")
if any(True for d in domains if not fqdn.match(d)):
raise errors.ConfigurationError("Requested domain is not a FQDN")

View file

@ -94,3 +94,7 @@ class StandaloneBindError(Error):
"Problem binding to port {0}: {1}".format(port, socket_error))
self.socket_error = socket_error
self.port = port
class ConfigurationError(Error):
"""Configuration sanity error."""

View file

@ -175,6 +175,24 @@ class CLITest(unittest.TestCase):
ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly'])
self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed')
def test_check_config_sanity_domain(self):
# Punycode
self.assertRaises(errors.ConfigurationError,
self._call,
['-d', 'this.is.xn--ls8h.tld'])
# FQDN
self.assertRaises(errors.ConfigurationError,
self._call,
['-d', 'comma,gotwrong.tld'])
# FQDN 2
self.assertRaises(errors.ConfigurationError,
self._call,
['-d', 'illegal.character=.tld'])
# Wildcard
self.assertRaises(errors.ConfigurationError,
self._call,
['-d', '*.wildcard.tld'])
@mock.patch('letsencrypt.crypto_util.notAfter')
@mock.patch('letsencrypt.cli.zope.component.getUtility')
def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter):

View file

@ -691,6 +691,7 @@ class RenewableCertTests(BaseRenewableCertTest):
self.test_rc.configfile["renewalparams"]["tls_sni_01_port"] = "4430"
self.test_rc.configfile["renewalparams"]["http01_port"] = "1234"
self.test_rc.configfile["renewalparams"]["account"] = "abcde"
self.test_rc.configfile["renewalparams"]["domains"] = ["example.com"]
mock_auth = mock.MagicMock()
mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth}
# Fails because "fake" != "apache"