Skip to content

Commit

Permalink
Merge branch 'config_sanity'
Browse files Browse the repository at this point in the history
  • Loading branch information
pde committed Nov 10, 2015
2 parents 4dc3ee7 + d29ab2a commit 6c3ea0d
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 21 deletions.
31 changes: 15 additions & 16 deletions letsencrypt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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
Expand All @@ -36,7 +37,6 @@

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


Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -159,9 +159,9 @@ def _tos_cb(regr):
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.")

Expand Down Expand Up @@ -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)

Expand All @@ -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


Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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?
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand Down
54 changes: 49 additions & 5 deletions letsencrypt/configuration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Let's Encrypt user-supplied configuration."""
import os
import urlparse
import re

import zope.interface

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -111,3 +109,49 @@ def renewal_configs_dir(self): # pylint: disable=missing-docstring
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")
4 changes: 4 additions & 0 deletions letsencrypt/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,7 @@ def __init__(self, socket_error, port):
"Problem binding to port {0}: {1}".format(port, socket_error))
self.socket_error = socket_error
self.port = port


class ConfigurationError(Error):
"""Configuration sanity error."""
18 changes: 18 additions & 0 deletions letsencrypt/tests/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,24 @@ def test_certonly_bad_args(self):
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):
Expand Down
1 change: 1 addition & 0 deletions letsencrypt/tests/renewer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,7 @@ def test_renew(self, mock_c, mock_acc_storage, mock_pd):
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"
Expand Down

0 comments on commit 6c3ea0d

Please sign in to comment.