From 687541505b9d1038e120d767bdf4a3f6e5d743f2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Jan 2015 02:48:14 +0000 Subject: [PATCH 01/19] IConfig, constants --- docs/api/client/constants.rst | 5 + letsencrypt/client/CONFIG.py | 84 +++++-------- letsencrypt/client/apache/configurator.py | 117 ++++++++++-------- letsencrypt/client/apache/dvsni.py | 7 +- letsencrypt/client/augeas_configurator.py | 15 +-- letsencrypt/client/auth_handler.py | 14 ++- letsencrypt/client/challenge_util.py | 10 +- letsencrypt/client/client.py | 117 +++++++++++------- letsencrypt/client/client_authenticator.py | 11 +- letsencrypt/client/constants.py | 41 ++++++ letsencrypt/client/crypto_util.py | 17 +-- letsencrypt/client/interfaces.py | 5 + letsencrypt/client/recovery_token.py | 4 +- letsencrypt/client/reverter.py | 20 +-- letsencrypt/client/revoker.py | 22 ++-- letsencrypt/client/tests/acme_util.py | 8 +- letsencrypt/client/tests/apache/dvsni_test.py | 8 +- letsencrypt/client/tests/apache/util.py | 13 +- .../client/tests/challenge_util_test.py | 6 +- .../client/tests/client_authenticator_test.py | 6 +- letsencrypt/client/tests/client_test.py | 2 +- letsencrypt/client/tests/reverter_test.py | 16 +-- letsencrypt/scripts/main.py | 14 +-- 23 files changed, 322 insertions(+), 240 deletions(-) create mode 100644 docs/api/client/constants.rst create mode 100644 letsencrypt/client/constants.py diff --git a/docs/api/client/constants.rst b/docs/api/client/constants.rst new file mode 100644 index 000000000..c901e13c2 --- /dev/null +++ b/docs/api/client/constants.rst @@ -0,0 +1,5 @@ +:mod:`letsencrypt.client.constants` +----------------------------------- + +.. automodule:: letsencrypt.client.constants + :members: diff --git a/letsencrypt/client/CONFIG.py b/letsencrypt/client/CONFIG.py index 5a07a4aa2..cc45601b5 100644 --- a/letsencrypt/client/CONFIG.py +++ b/letsencrypt/client/CONFIG.py @@ -1,6 +1,13 @@ """Config for Let's Encrypt.""" import os.path +import zope.component + +from letsencrypt.client import interfaces + + +zope.component.moduleProvides(interfaces.IConfig) + ACME_SERVER = "letsencrypt-demo.org:443" """CA hostname (and optionally :port). @@ -10,9 +17,6 @@ If you create your own server... change this line Note: the server certificate must be trusted in order to avoid further modifications to the client.""" -# Directories -SERVER_ROOT = "/etc/apache2/" -"""Apache server root directory""" CONFIG_DIR = "/etc/letsencrypt/" """Configuration file directory for letsencrypt""" @@ -35,69 +39,43 @@ CERT_KEY_BACKUP = os.path.join(WORK_DIR, "keys-certs/") REV_TOKENS_DIR = os.path.join(WORK_DIR, "revocation_tokens/") """Directory where all revocation tokens are saved.""" -KEY_DIR = os.path.join(SERVER_ROOT, "keys/") -"""Where all keys should be stored""" +KEY_DIR = os.path.join(CONFIG_DIR, "keys/") +"""Keys storage.""" -CERT_DIR = os.path.join(SERVER_ROOT, "certs/") -"""Certificate storage""" +CERT_DIR = os.path.join(CONFIG_DIR, "certs/") +"""Certificate storage.""" -# Files and extensions -OPTIONS_SSL_CONF = os.path.join(CONFIG_DIR, "options-ssl.conf") -"""Contains standard Apache SSL directives""" LE_VHOST_EXT = "-le-ssl.conf" -"""Let's Encrypt SSL vhost configuration extension""" +"""Let's Encrypt SSL vhost configuration extension.""" -CERT_PATH = CERT_DIR + "cert-letsencrypt.pem" +CERT_PATH = os.path.join(CERT_DIR, "cert-letsencrypt.pem") """Let's Encrypt cert file.""" -CHAIN_PATH = CERT_DIR + "chain-letsencrypt.pem" +CHAIN_PATH = os.path.join(CERT_DIR, "chain-letsencrypt.pem") """Let's Encrypt chain file.""" -INVALID_EXT = ".acme.invalid" -"""Invalid Extension""" -# Challenge Sets -EXCLUSIVE_CHALLENGES = [frozenset(["dvsni", "simpleHttps"])] -"""Mutually Exclusive Challenges - only solve 1""" - -DV_CHALLENGES = frozenset(["dvsni", "simpleHttps", "dns"]) -"""These are challenges that must be solved by an Authenticator object""" - -CLIENT_CHALLENGES = frozenset( - ["recoveryToken", "recoveryContact", "proofOfPossession"]) -"""These are challenges that are handled by client.py""" - -# Challenge Constants -S_SIZE = 32 -"""Byte size of S""" - -NONCE_SIZE = 16 -"""byte size of Nonce""" - -# Key Sizes RSA_KEY_SIZE = 2048 """Key size""" -# Enhancements -ENHANCEMENTS = ["redirect", "http-header", "ocsp-stapling", "spdy"] -"""List of possible IInstaller enhancements. -List of expected options parameters: -redirect, None -http-header, TODO -ocsp-stapling, TODO -spdy, TODO - -""" -# Apache Enhancement Arguments -REWRITE_HTTPS_ARGS = [ - "^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"] -"""Rewrite rule arguments used for redirections to https vhost""" - -# Apache Interaction APACHE_CTL = "/usr/sbin/apache2ctl" -"""Command used for configtest and version number.""" +"""Path to the ``apache2ctl`` binary, used for ``configtest`` and +retrieving Apache2 version number.""" -APACHE2 = "/etc/init.d/apache2" -"""Command used for reload and restart.""" +APACHE_ENMOD = "apache" +"""Path to the Apache ``a2enmod`` binary.""" + +APACHE_INIT_SCRIPT = "/etc/init.d/apache2" +"""Path to the Apache init script (used for server reload/restart).""" + +APACHE_REWRITE_HTTPS_ARGS = [ + "^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"] +"""Apache rewrite rule arguments used for redirections to https vhost""" + +APACHE_SERVER_ROOT = "/etc/apache2/" +"""Apache server root directory""" + +APACHE_MOD_SSL_CONF = os.path.join(CONFIG_DIR, "options-ssl.conf") +"""Contains standard Apache SSL directives""" diff --git a/letsencrypt/client/apache/configurator.py b/letsencrypt/client/apache/configurator.py index ad6e54273..e5b955b27 100644 --- a/letsencrypt/client/apache/configurator.py +++ b/letsencrypt/client/apache/configurator.py @@ -1,7 +1,6 @@ """Apache Configuration based off of Augeas Configurator.""" import logging import os -import pkg_resources import re import shutil import socket @@ -12,7 +11,7 @@ import zope.interface from letsencrypt.client import augeas_configurator from letsencrypt.client import challenge_util -from letsencrypt.client import CONFIG +from letsencrypt.client import constants from letsencrypt.client import errors from letsencrypt.client import interfaces from letsencrypt.client import le_util @@ -63,6 +62,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): The API of this class will change in the coming weeks as the exact needs of clients are clarified with the new and developing protocol. + :ivar config: Configuration. + :type config: :class:`~letsencrypt.client.interfaces.IConfig` + :ivar str server_root: Path to Apache root directory :ivar dict location: Path to various files associated with the configuration @@ -75,27 +77,27 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ zope.interface.implements(interfaces.IAuthenticator, interfaces.IInstaller) - def __init__(self, server_root=CONFIG.SERVER_ROOT, direc=None, - ssl_options=CONFIG.OPTIONS_SSL_CONF, version=None): + def __init__(self, config, direc=None, version=None): """Initialize an Apache Configurator. - :param str server_root: the apache server root directory :param dict direc: locations of various config directories (used mostly for unittesting) - :param str ssl_options: path of options-ssl.conf - (used mostly for unittesting) :param tup version: version of Apache as a tuple (2, 4, 7) (used mostly for unittesting) """ - if direc is None: - direc = {"backup": CONFIG.BACKUP_DIR, - "temp": CONFIG.TEMP_CHECKPOINT_DIR, - "progress": CONFIG.IN_PROGRESS_DIR, - "config": CONFIG.CONFIG_DIR, - "work": CONFIG.WORK_DIR} + self.config = config + server_root = self.config.APACHE_SERVER_ROOT + ssl_options = self.config.APACHE_MOD_SSL_CONF - super(ApacheConfigurator, self).__init__(direc) + if direc is None: + direc = {"backup": self.config.BACKUP_DIR, + "temp": self.config.TEMP_CHECKPOINT_DIR, + "progress": self.config.IN_PROGRESS_DIR, + "config": self.config.CONFIG_DIR, + "work": self.config.WORK_DIR} + + super(ApacheConfigurator, self).__init__(config, direc) self.direc = direc # Verify that all directories and files exist with proper permissions @@ -382,9 +384,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): is appropriately listening on port 443. """ - if not mod_loaded("ssl_module"): + if not mod_loaded("ssl_module", self.config.APACHE_CTL): logging.info("Loading mod_ssl into Apache Server") - enable_mod("ssl") + enable_mod("ssl", self.config.APACHE_INIT_SCRIPT, + self.config.APACHE_ENMOD) # Check for Listen 443 # Note: This could be made to also look for ip:443 combo @@ -427,7 +430,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """Makes an ssl_vhost version of a nonssl_vhost. Duplicates vhost and adds default ssl options - New vhost will reside as (nonssl_vhost.path) + CONFIG.LE_VHOST_EXT + New vhost will reside as (nonssl_vhost.path) + ``IConfig.LE_VHOST_EXT`` + .. note:: This function saves the configuration :param nonssl_vhost: Valid VH that doesn't have SSLEngine on @@ -440,27 +444,24 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): avail_fp = nonssl_vhost.filep # Get filepath of new ssl_vhost if avail_fp.endswith(".conf"): - ssl_fp = avail_fp[:-(len(".conf"))] + CONFIG.LE_VHOST_EXT + ssl_fp = avail_fp[:-(len(".conf"))] + self.config.LE_VHOST_EXT else: - ssl_fp = avail_fp + CONFIG.LE_VHOST_EXT + ssl_fp = avail_fp + self.config.LE_VHOST_EXT # First register the creation so that it is properly removed if # configuration is rolled back self.reverter.register_file_creation(False, ssl_fp) try: - orig_file = open(avail_fp, 'r') - new_file = open(ssl_fp, 'w') - new_file.write("\n") - for line in orig_file: - new_file.write(line) - new_file.write("\n") + with open(avail_fp, 'r') as orig_file: + with open(ssl_fp, 'w') as new_file: + new_file.write("\n") + for line in orig_file: + new_file.write(line) + new_file.write("\n") except IOError: logging.fatal("Error writing/reading to file in make_vhost_ssl") sys.exit(49) - finally: - orig_file.close() - new_file.close() self.aug.load() @@ -528,9 +529,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param str domain: domain to enhance :param str enhancement: enhancement type defined in - :class:`letsencrypt.client.CONFIG.ENHANCEMENTS` + :const:`~letsencrypt.client.constants.ENHANCEMENTS` :param options: options for the enhancement - :type options: See :class:`letsencrypt.client.CONFIG.ENHANCEMENTS` + See :const:`~letsencrypt.client.constants.ENHANCEMENTS` documentation for appropriate parameter. """ @@ -565,8 +566,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :rtype: (bool, :class:`letsencrypt.client.apache.obj.VirtualHost`) """ - if not mod_loaded("rewrite_module"): - enable_mod("rewrite") + if not mod_loaded("rewrite_module", self.config.APACHE_CTL): + enable_mod("rewrite", self.config.APACHE_INIT_SCRIPT, + self.config.APACHE_ENMOD) general_v = self._general_vhost(ssl_vhost) if general_v is None: @@ -590,8 +592,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "in {}".format(general_v.filep)) # Add directives to server self.parser.add_dir(general_v.path, "RewriteEngine", "On") - self.parser.add_dir( - general_v.path, "RewriteRule", CONFIG.REWRITE_HTTPS_ARGS) + self.parser.add_dir(general_v.path, "RewriteRule", + self.config.APACHE_REWRITE_HTTPS_ARGS) self.save_notes += ('Redirecting host in %s to ssl vhost in %s\n' % (general_v.filep, ssl_vhost.filep)) self.save() @@ -630,9 +632,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if not rewrite_path: # "No existing redirection for virtualhost" return False, -1 - if len(rewrite_path) == len(CONFIG.REWRITE_HTTPS_ARGS): + if len(rewrite_path) == len(self.config.APACHE_REWRITE_HTTPS_ARGS): for idx, match in enumerate(rewrite_path): - if self.aug.get(match) != CONFIG.REWRITE_HTTPS_ARGS[idx]: + if (self.aug.get(match) != + self.config.APACHE_REWRITE_HTTPS_ARGS[idx]): # Not a letsencrypt https rewrite return True, 2 # Existing letsencrypt https rewrite rule is in place @@ -681,7 +684,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "LogLevel warn\n" "\n" % (servername, serveralias, - " ".join(CONFIG.REWRITE_HTTPS_ARGS))) + " ".join(self.config.APACHE_REWRITE_HTTPS_ARGS))) # Write out the file # This is the default name @@ -876,14 +879,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return True return False - def restart(self): # pylint: disable=no-self-use + def restart(self): """Restarts apache server. :returns: Success :rtype: bool """ - return apache_restart() + return apache_restart(self.config.APACHE_INIT_SCRIPT) def config_test(self): # pylint: disable=no-self-use """Check the configuration of Apache for errors. @@ -894,7 +897,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: proc = subprocess.Popen( - ['sudo', '/usr/sbin/apache2ctl', 'configtest'], + ['sudo', self.config.APACHE_CTL, 'configtest'], # TODO: sudo? stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() @@ -925,13 +928,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: proc = subprocess.Popen( - [CONFIG.APACHE_CTL, '-v'], + [self.config.APACHE_CTL, '-v'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) text = proc.communicate()[0] except (OSError, ValueError): raise errors.LetsEncryptConfiguratorError( - "Unable to run %s -v" % CONFIG.APACHE_CTL) + "Unable to run %s -v" % self.config.APACHE_CTL) regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE) matches = regex.findall(text) @@ -1012,47 +1015,51 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.restart() -def enable_mod(mod_name): +def enable_mod(mod_name, apache_init_script, apache_enmod): """Enables module in Apache. Both enables and restarts Apache so module is active. - :param str mod_name: Name of the module to enable + :param str mod_name: Name of the module to enable. + :param str apache_init_script: Path to the Apache init script. + :param str apache_enmod: Path to the Apache a2enmod script. """ try: # Use check_output so the command will finish before reloading # TODO: a2enmod is debian specific... - subprocess.check_call(["sudo", "a2enmod", mod_name], + subprocess.check_call(["sudo", apache_enmod, mod_name], # TODO: sudo? stdout=open("/dev/null", 'w'), stderr=open("/dev/null", 'w')) - apache_restart() + apache_restart(apache_init_script) except (OSError, subprocess.CalledProcessError) as err: logging.error("Error enabling mod_%s", mod_name) logging.error("Exception: %s", err) sys.exit(1) -def mod_loaded(module): +def mod_loaded(module, apache_ctl): """Checks to see if mod_ssl is loaded - Uses CONFIG.APACHE_CTL to get loaded module list. This also effectively + Uses ``apache_ctl`` to get loaded module list. This also effectively serves as a config_test. + :param str apache_ctl: Path to apache2ctl binary. + :returns: If ssl_module is included and active in Apache :rtype: bool """ try: proc = subprocess.Popen( - [CONFIG.APACHE_CTL, '-M'], + [apache_ctl, '-M'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() except (OSError, ValueError): logging.error( - "Error accessing %s for loaded modules!", CONFIG.APACHE_CTL) + "Error accessing %s for loaded modules!", apache_ctl) raise errors.LetsEncryptConfiguratorError( "Error accessing loaded modules") # Small errors that do not impede @@ -1067,9 +1074,11 @@ def mod_loaded(module): return False -def apache_restart(): +def apache_restart(apache_init_script): """Restarts the Apache Server. + :param str apache_init_script: Path to the Apache init script. + .. todo:: Try to use reload instead. (This caused timing problems before) .. todo:: On failure, this should be a recovery_routine call with another @@ -1081,7 +1090,7 @@ def apache_restart(): """ try: - proc = subprocess.Popen([CONFIG.APACHE2, 'restart'], + proc = subprocess.Popen([apache_init_script, 'restart'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() @@ -1138,6 +1147,4 @@ def temp_install(options_ssl): # Check to make sure options-ssl.conf is installed if not os.path.isfile(options_ssl): - dist_conf = pkg_resources.resource_filename( - __name__, os.path.basename(options_ssl)) - shutil.copyfile(dist_conf, options_ssl) + shutil.copyfile(constants.APACHE_MOD_SSL_CONF, options_ssl) diff --git a/letsencrypt/client/apache/dvsni.py b/letsencrypt/client/apache/dvsni.py index d514dbbdb..242c0d324 100644 --- a/letsencrypt/client/apache/dvsni.py +++ b/letsencrypt/client/apache/dvsni.py @@ -3,7 +3,7 @@ import logging import os from letsencrypt.client import challenge_util -from letsencrypt.client import CONFIG +from letsencrypt.client import constants from letsencrypt.client.apache import parser @@ -12,7 +12,8 @@ class ApacheDvsni(object): """Class performs DVSNI challenges within the Apache configurator. :ivar config: ApacheConfigurator object - :type config: :class:`letsencrypt.client.apache.configurator` + :type config: + :class:`letsencrypt.client.apache.configurator.ApacheConfigurator` :ivar dvsni_chall: Data required for challenges. where DvsniChall tuples have the following fields @@ -165,7 +166,7 @@ class ApacheDvsni(object): """ ips = " ".join(str(i) for i in ip_addrs) return ("\n" - "ServerName " + nonce + CONFIG.INVALID_EXT + "\n" + "ServerName " + nonce + constants.DVSNI_DOMAIN_SUFFIX + "\n" "UseCanonicalName on\n" "SSLStrictSNIVHostCheck on\n" "\n" diff --git a/letsencrypt/client/augeas_configurator.py b/letsencrypt/client/augeas_configurator.py index c35df5c2d..57f0feae0 100644 --- a/letsencrypt/client/augeas_configurator.py +++ b/letsencrypt/client/augeas_configurator.py @@ -3,7 +3,6 @@ import logging import augeas -from letsencrypt.client import CONFIG from letsencrypt.client import reverter @@ -20,18 +19,20 @@ class AugeasConfigurator(object): """ - def __init__(self, direc=None): + def __init__(self, config, direc=None): """Initialize Augeas Configurator. + :param config: Configuration. + :type config: :class:`~letsencrypt.client.interfaces.IConfig` + :param dict direc: location of save directories (used mostly for testing) """ - if not direc: - direc = {"backup": CONFIG.BACKUP_DIR, - "temp": CONFIG.TEMP_CHECKPOINT_DIR, - "progress": CONFIG.IN_PROGRESS_DIR} + direc = {"backup": config.BACKUP_DIR, + "temp": config.TEMP_CHECKPOINT_DIR, + "progress": config.IN_PROGRESS_DIR} # Set Augeas flags to not save backup (we do it ourselves) # Set Augeas to not load anything by default @@ -43,7 +44,7 @@ class AugeasConfigurator(object): # This needs to occur before VirtualHost objects are setup... # because this will change the underlying configuration and potential # vhosts - self.reverter = reverter.Reverter(direc) + self.reverter = reverter.Reverter(config, direc) self.reverter.recovery_routine() def check_parsing_errors(self, lens): diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index b85996818..5e94c362e 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -3,18 +3,20 @@ import logging import sys from letsencrypt.client import acme -from letsencrypt.client import CONFIG from letsencrypt.client import challenge_util +from letsencrypt.client import constants from letsencrypt.client import errors class AuthHandler(object): # pylint: disable=too-many-instance-attributes """ACME Authorization Handler for a client. - :ivar dv_auth: Authenticator capable of solving CONFIG.DV_CHALLENGES + :ivar dv_auth: Authenticator capable of solving + :const:`~letsencrypt.client.constants.DV_CHALLENGES` :type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator` - :ivar client_auth: Authenticator capable of solving CONFIG.CLIENT_CHALLENGES + :ivar client_auth: Authenticator capable of solving + :const:`~letsencrypt.client_auth.constants.CLIENT_CHALLENGES` :type client_auth: :class:`letsencrypt.client.interfaces.IAuthenticator` :ivar network: Network object for sending and receiving authorization @@ -238,12 +240,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes chall = challenges[index] # Authenticator Challenges - if chall["type"] in CONFIG.DV_CHALLENGES: + if chall["type"] in constants.DV_CHALLENGES: dv_chall.append(challenge_util.IndexedChall( self._construct_dv_chall(chall, domain), index)) # Client Challenges - elif chall["type"] in CONFIG.CLIENT_CHALLENGES: + elif chall["type"] in constants.CLIENT_CHALLENGES: client_chall.append(challenge_util.IndexedChall( self._construct_client_chall(chall, domain), index)) @@ -430,7 +432,7 @@ def _find_dumb_path(challenges, preferences): def is_preferred(offered_challenge_type, path): """Return whether or not the challenge is preferred in path.""" for _, challenge_type in path: - for mutually_exclusive in CONFIG.EXCLUSIVE_CHALLENGES: + for mutually_exclusive in constants.EXCLUSIVE_CHALLENGES: # Second part is in case we eventually allow multiple names # to be challenges at the same time if (challenge_type in mutually_exclusive and diff --git a/letsencrypt/client/challenge_util.py b/letsencrypt/client/challenge_util.py index b5d1cf38d..e4001d143 100644 --- a/letsencrypt/client/challenge_util.py +++ b/letsencrypt/client/challenge_util.py @@ -4,7 +4,7 @@ import hashlib from Crypto import Random -from letsencrypt.client import CONFIG +from letsencrypt.client import constants from letsencrypt.client import crypto_util from letsencrypt.client import le_util @@ -44,14 +44,14 @@ def dvsni_gen_cert(name, r_b64, nonce, key): """ # Generate S - dvsni_s = Random.get_random_bytes(CONFIG.S_SIZE) + dvsni_s = Random.get_random_bytes(constants.S_SIZE) dvsni_r = le_util.jose_b64decode(r_b64) # Generate extension ext = _dvsni_gen_ext(dvsni_r, dvsni_s) cert_pem = crypto_util.make_ss_cert( - key.pem, [nonce + CONFIG.INVALID_EXT, name, ext]) + key.pem, [nonce + constants.DVSNI_DOMAIN_SUFFIX, name, ext]) return cert_pem, le_util.jose_b64encode(dvsni_s) @@ -62,7 +62,7 @@ def _dvsni_gen_ext(dvsni_r, dvsni_s): :param bytearray dvsni_r: DVSNI r value :param bytearray dvsni_s: DVSNI s value - :returns: z + CONFIG.INVALID_EXT + :returns: z + :const:`~letsencrypt.client.constants.DVSNI_DOMAIN_SUFFIX` :rtype: str """ @@ -70,4 +70,4 @@ def _dvsni_gen_ext(dvsni_r, dvsni_s): z_base.update(dvsni_r) z_base.update(dvsni_s) - return z_base.hexdigest() + CONFIG.INVALID_EXT + return z_base.hexdigest() + constants.DVSNI_DOMAIN_SUFFIX diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 223a1ce3a..9c87a1acb 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -12,7 +12,6 @@ import zope.component from letsencrypt.client import acme from letsencrypt.client import auth_handler from letsencrypt.client import client_authenticator -from letsencrypt.client import CONFIG from letsencrypt.client import crypto_util from letsencrypt.client import errors from letsencrypt.client import interfaces @@ -40,6 +39,9 @@ class Client(object): :ivar installer: Object supporting the IInstaller interface. :type installer: :class:`letsencrypt.client.interfaces.IInstaller` + :ivar config: Configuration. + :type config: :class:`~letsencrypt.client.interfaces.IConfig` + """ zope.interface.implements(interfaces.IAuthenticator) @@ -47,12 +49,12 @@ class Client(object): # Note: form is the type of data, "pem" or "der" CSR = collections.namedtuple("CSR", "file data form") - def __init__(self, server, authkey, dv_auth, installer): + def __init__(self, server, authkey, dv_auth, installer, config): """Initialize a client. :param str server: CA server to contact - :param dv_auth: IAuthenticator Interface that can solve the - CONFIG.DV_CHALLENGES + :param dv_auth: IAuthenticator that can solve the + :const:`letsencrypt.client.constants.DV_CHALLENGES` :type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator` """ @@ -61,30 +63,32 @@ class Client(object): self.installer = installer + self.config = config + if dv_auth is not None: - client_auth = client_authenticator.ClientAuthenticator(server) + client_auth = client_authenticator.ClientAuthenticator( + server, config) self.auth_handler = auth_handler.AuthHandler( dv_auth, client_auth, self.network) else: self.auth_handler = None - def obtain_certificate(self, domains, csr=None, - cert_path=CONFIG.CERT_PATH, - chain_path=CONFIG.CHAIN_PATH): + def obtain_certificate(self, domains, csr=None): """Obtains a certificate from the ACME server. :param str domains: list of domains to get a certificate + :param csr: CSR must contain requested domains, the key used to generate this CSR can be different than self.authkey :type csr: :class:`CSR` - :param str cert_path: Full desired path to end certificate. - :param str chain_path: Full desired path to end chain file. - :returns: cert_file, chain_file (paths to respective files) :rtype: `tuple` of `str` """ + cert_path = self.config.CERT_PATH + chain_path = self.config.CHAIN_PATH + if self.auth_handler is None: logging.warning("Unable to obtain a certificate, because client " "does not have a valid auth handler.") @@ -99,7 +103,7 @@ class Client(object): # Create CSR from names if csr is None: - csr = init_csr(self.authkey, domains) + csr = init_csr(self.authkey, domains, self.config.CERT_DIR) # Retrieve certificate certificate_dict = self.acme_certificate(csr.data) @@ -241,8 +245,8 @@ class Client(object): :rtype: bool """ - list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST") - le_util.make_or_verify_dir(CONFIG.CERT_KEY_BACKUP, 0o700) + list_file = os.path.join(self.config.CERT_KEY_BACKUP, "LIST") + le_util.make_or_verify_dir(self.config.CERT_KEY_BACKUP, 0o700) idx = 0 if encrypt: @@ -267,11 +271,11 @@ class Client(object): shutil.copy2(self.authkey.file, os.path.join( - CONFIG.CERT_KEY_BACKUP, + self.config.CERT_KEY_BACKUP, os.path.basename(self.authkey.file) + "_" + str(idx))) shutil.copy2(cert_file, os.path.join( - CONFIG.CERT_KEY_BACKUP, + self.config.CERT_KEY_BACKUP, os.path.basename(cert_file) + "_" + str(idx))) return True @@ -339,7 +343,7 @@ def validate_key_csr(privkey, csr=None): "The key and CSR do not match") -def init_key(key_size): +def init_key(key_size, key_dir): """Initializes privkey. Inits key and CSR using provided files or generating new files @@ -347,19 +351,19 @@ def init_key(key_size): filesystem. The CSR is placed into DER format to allow the namedtuple to easily work with the protocol. + :param str key_dir: Key save directory. + """ try: key_pem = crypto_util.make_key(key_size) except ValueError as err: logging.fatal(str(err)) - logging.info("Note: The default RSA key size is %d bits.", - CONFIG.RSA_KEY_SIZE) sys.exit(1) # Save file - le_util.make_or_verify_dir(CONFIG.KEY_DIR, 0o700) + le_util.make_or_verify_dir(key_dir, 0o700) key_f, key_filename = le_util.unique_file( - os.path.join(CONFIG.KEY_DIR, "key-letsencrypt.pem"), 0o600) + os.path.join(key_dir, "key-letsencrypt.pem"), 0o600) key_f.write(key_pem) key_f.close() @@ -368,15 +372,18 @@ def init_key(key_size): return Client.Key(key_filename, key_pem) -def init_csr(privkey, names): - """Initialize a CSR with the given private key.""" +def init_csr(privkey, names, cert_dir): + """Initialize a CSR with the given private key. + :param str cert_dir: Certificate save directory. + + """ csr_pem, csr_der = crypto_util.make_csr(privkey.pem, names) # Save CSR - le_util.make_or_verify_dir(CONFIG.CERT_DIR, 0o755) + le_util.make_or_verify_dir(cert_dir, 0o755) csr_f, csr_filename = le_util.unique_file( - os.path.join(CONFIG.CERT_DIR, "csr-letsencrypt.pem"), 0o644) + os.path.join(cert_dir, "csr-letsencrypt.pem"), 0o644) csr_f.write(csr_pem) csr_f.close() @@ -393,23 +400,33 @@ def csr_pem_to_der(csr): # This should be controlled by commandline parameters -def determine_authenticator(): - """Returns a valid IAuthenticator.""" +def determine_authenticator(config): + """Returns a valid IAuthenticator. + + :param config: Configuration. + :type config: :class:`letsencrypt.client.interfaces.IConfig` + + """ try: - return configurator.ApacheConfigurator() + return configurator.ApacheConfigurator(config) except errors.LetsEncryptNoInstallationError: logging.info("Unable to determine a way to authenticate the server") -def determine_installer(): - """Returns a valid installer if one exists.""" +def determine_installer(config): + """Returns a valid installer if one exists. + + :param config: Configuration. + :type config: :class:`letsencrypt.client.interfaces.IConfig` + + """ try: - return configurator.ApacheConfigurator() + return configurator.ApacheConfigurator(config) except errors.LetsEncryptNoInstallationError: logging.info("Unable to find a way to install the certificate.") -def rollback(checkpoints): +def rollback(checkpoints, config): """Revert configuration the specified number of checkpoints. .. note:: If another installer uses something other than the reverter class @@ -426,12 +443,15 @@ def rollback(checkpoints): :param int checkpoints: Number of checkpoints to revert. + :param config: Configuration. + :type config: :class:`letsencrypt.client.interfaces.IConfig` + """ # Misconfigurations are only a slight problems... allow the user to rollback try: - installer = determine_installer() + installer = determine_installer(config) except errors.LetsEncryptMisconfigurationError: - _misconfigured_rollback(checkpoints) + _misconfigured_rollback(checkpoints, config) return # No Errors occurred during init... proceed normally @@ -442,8 +462,13 @@ def rollback(checkpoints): installer.restart() -def _misconfigured_rollback(checkpoints): - """Handles the case where the Installer is misconfigured.""" +def _misconfigured_rollback(checkpoints, config): + """Handles the case where the Installer is misconfigured. + + :param config: Configuration. + :type config: :class:`letsencrypt.client.interfaces.IConfig` + + """ yes = zope.component.getUtility(interfaces.IDisplay).generic_yesno( "Oh, no! The web server is currently misconfigured.{0}{0}" "Would you still like to rollback the " @@ -457,13 +482,13 @@ def _misconfigured_rollback(checkpoints): # recovery routine has probably already been run by installer # in the__init__ attempt, run it again for safety... it shouldn't hurt # Also... not sure how future installers will handle recovery. - rev = reverter.Reverter() + rev = reverter.Reverter(config) rev.recovery_routine() rev.rollback_checkpoints(checkpoints) # We should try to restart the server try: - installer = determine_installer() + installer = determine_installer(config) installer.restart() logging.info("Hooray! Rollback solved the misconfiguration!") logging.info("Your web server is back up and running.") @@ -472,16 +497,19 @@ def _misconfigured_rollback(checkpoints): "Rollback was unable to solve the misconfiguration issues") -def revoke(server): +def revoke(server, config): """Revoke certificates. :param str server: ACME server the client wishes to revoke certificates from + :param config: Configuration. + :type config: :class:`letsencrypt.client.interfaces.IConfig` + """ # Misconfigurations don't really matter. Determine installer better choose # correctly though. try: - installer = determine_installer() + installer = determine_installer(config) except errors.LetsEncryptMisconfigurationError: zope.component.getUtility(interfaces.IDisplay).generic_notification( "The web server is currently misconfigured. Some " @@ -497,16 +525,19 @@ def revoke(server): "revocation without a valid installer. This feature should come " "soon.") return - revoc = revoker.Revoker(server, installer) + revoc = revoker.Revoker(server, installer, config) revoc.list_certs_keys() -def view_config_changes(): +def view_config_changes(config): """View checkpoints and associated configuration changes. .. note:: This assumes that the installation is using a Reverter object. + :param config: Configuration. + :type config: :class:`letsencrypt.client.interfaces.IConfig` + """ - rev = reverter.Reverter() + rev = reverter.Reverter(config) rev.recovery_routine() rev.view_config_changes() diff --git a/letsencrypt/client/client_authenticator.py b/letsencrypt/client/client_authenticator.py index 699e5e598..4a33f719b 100644 --- a/letsencrypt/client/client_authenticator.py +++ b/letsencrypt/client/client_authenticator.py @@ -8,7 +8,8 @@ from letsencrypt.client import recovery_token class ClientAuthenticator(object): - """IAuthenticator for CONFIG.CLIENT_CHALLENGES. + """IAuthenticator for + :const:`~letsencrypt.client.constants.CLIENT_CHALLENGES`. :ivar rec_token: Performs "recoveryToken" challenges :type rec_token: :class:`letsencrypt.client.recovery_token.RecoveryToken` @@ -17,13 +18,17 @@ class ClientAuthenticator(object): zope.interface.implements(interfaces.IAuthenticator) # This will have an installer soon for get_key/cert purposes - def __init__(self, server): + def __init__(self, server, config): """Initialize Client Authenticator. :param str server: ACME CA Server + :param config: Configuration. + :type config: :class:`letsencrypt.client.interfaces.IConfig` + """ - self.rec_token = recovery_token.RecoveryToken(server) + self.rec_token = recovery_token.RecoveryToken( + server, config.REV_TOKEN_DIRS) def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" diff --git a/letsencrypt/client/constants.py b/letsencrypt/client/constants.py new file mode 100644 index 000000000..bf6d6986b --- /dev/null +++ b/letsencrypt/client/constants.py @@ -0,0 +1,41 @@ +"""Let's Encrypt constants.""" +import pkg_resources + + +S_SIZE = 32 +"""Size (in bytes) of secret base64-encoded octet string "s" used in +challanges.""" + +NONCE_SIZE = 16 +"""Size of nonce used in JWS objects (in bytes).""" + +EXCLUSIVE_CHALLENGES = [frozenset(["dvsni", "simpleHttps"])] +"""Mutually exclusive challenges.""" + +DV_CHALLENGES = frozenset(["dvsni", "simpleHttps", "dns"]) +"""Challenges that must be solved by a +:class:`letsencrypt.client.interfaces.IAuthenticator` object.""" + +CLIENT_CHALLENGES = frozenset( + ["recoveryToken", "recoveryContact", "proofOfPossession"]) +"""Challenges that are handled by the Let's Encrypt client.""" + +ENHANCEMENTS = ["redirect", "http-header", "ocsp-stapling", "spdy"] +"""List of possible :class:`letsencrypt.client.interfaces.IInstaller` +enhancements. + +List of expected options parameters: +- redirect: None +- http-header: TODO +- ocsp-stapling: TODO +- spdy: TODO + +""" + +APACHE_MOD_SSL_CONF = pkg_resources.resource_filename( + 'letsencrypt.client.apache', 'options-ssl.conf') +"""Path to the Apache mod_ssl config file found in the Let's Encrypt +distribution.""" + +DVSNI_DOMAIN_SUFFIX = ".acme.invalid" +"""Suffix appended to domains in DVSNI validation.""" diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py index c1f59aa45..9b038f0de 100644 --- a/letsencrypt/client/crypto_util.py +++ b/letsencrypt/client/crypto_util.py @@ -10,11 +10,11 @@ import Crypto.Signature.PKCS1_v1_5 import M2Crypto -from letsencrypt.client import CONFIG +from letsencrypt.client import constants from letsencrypt.client import le_util -def create_sig(msg, key_str, nonce=None, nonce_len=CONFIG.NONCE_SIZE): +def create_sig(msg, key_str, nonce=None): """Create signature with nonce prepended to the message. .. todo:: Change this over to M2Crypto... PKey @@ -24,22 +24,17 @@ def create_sig(msg, key_str, nonce=None, nonce_len=CONFIG.NONCE_SIZE): :param str key_str: Key in string form. Accepted formats are the same as for `Crypto.PublicKey.RSA.importKey`. - :param str msg: Message to be signed - - :param nonce: Nonce to be used. If None, nonce of `nonce_len` size - will be randomly generated. - :type nonce: str or None - - :param int nonce_len: Size of the automatically generated nonce. + :param str nonce: Nonce to be used (required size :returns: Signature. :rtype: dict """ - msg = str(msg) key = Crypto.PublicKey.RSA.importKey(key_str) - nonce = Random.get_random_bytes(nonce_len) if nonce is None else nonce + if nonce is None: + nonce = Random.get_random_bytes(constants.NONCE_SIZE) + assert len(nonce) == constants.NONCE_SIZE msg_with_nonce = nonce + msg hashed = Crypto.Hash.SHA256.new(msg_with_nonce) diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index 1c6d4766f..c4f28afdb 100644 --- a/letsencrypt/client/interfaces.py +++ b/letsencrypt/client/interfaces.py @@ -2,6 +2,7 @@ import zope.interface # pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class +# pylint: disable=too-few-public-methods class IAuthenticator(zope.interface.Interface): @@ -58,6 +59,10 @@ class IChallenge(zope.interface.Interface): """Cleanup.""" +class IConfig(zope.interface.Interface): + """Marker interface for Let's Encrypt config.""" + + class IInstaller(zope.interface.Interface): """Generic Let's Encrypt Installer Interface. diff --git a/letsencrypt/client/recovery_token.py b/letsencrypt/client/recovery_token.py index 2da8a9c3f..b7cc2d64c 100644 --- a/letsencrypt/client/recovery_token.py +++ b/letsencrypt/client/recovery_token.py @@ -3,9 +3,7 @@ import errno import os import zope.component -# import zope.interface -from letsencrypt.client import CONFIG from letsencrypt.client import le_util from letsencrypt.client import interfaces @@ -16,7 +14,7 @@ class RecoveryToken(object): Based on draft-barnes-acme, section 6.4. """ - def __init__(self, server, direc=CONFIG.REV_TOKENS_DIR): + def __init__(self, server, direc): self.token_dir = os.path.join(direc, server) def perform(self, chall): diff --git a/letsencrypt/client/reverter.py b/letsencrypt/client/reverter.py index 4bb2bd46c..f60204a76 100644 --- a/letsencrypt/client/reverter.py +++ b/letsencrypt/client/reverter.py @@ -6,7 +6,6 @@ import time import zope.component -from letsencrypt.client import CONFIG from letsencrypt.client import display from letsencrypt.client import errors from letsencrypt.client import interfaces @@ -14,12 +13,19 @@ from letsencrypt.client import le_util class Reverter(object): - """Reverter Class - save and revert configuration checkpoints""" - def __init__(self, direc=None): - if not direc: - direc = {'backup': CONFIG.BACKUP_DIR, - 'temp': CONFIG.TEMP_CHECKPOINT_DIR, - 'progress': CONFIG.IN_PROGRESS_DIR} + """Reverter Class - save and revert configuration checkpoints.""" + + def __init__(self, config, direc=None): + """Initialize Reverter. + + :param config: Configuration. + :type config: :class:`letsencrypt.client.interfaces.IConfig` + + """ + if direc is None: + direc = {'backup': config.BACKUP_DIR, + 'temp': config.TEMP_CHECKPOINT_DIR, + 'progress': config.IN_PROGRESS_DIR} self.direc = direc def revert_temporary_config(self): diff --git a/letsencrypt/client/revoker.py b/letsencrypt/client/revoker.py index f8b75b39c..6093b2dd2 100644 --- a/letsencrypt/client/revoker.py +++ b/letsencrypt/client/revoker.py @@ -8,7 +8,6 @@ import M2Crypto import zope.component from letsencrypt.client import acme -from letsencrypt.client import CONFIG from letsencrypt.client import crypto_util from letsencrypt.client import display from letsencrypt.client import interfaces @@ -16,10 +15,17 @@ from letsencrypt.client import network class Revoker(object): - """A revocation class for LE.""" - def __init__(self, server, installer): + """A revocation class for LE. + + :param config: Configuration. + :type config: :class:`letsencrypt.client.interfaces.IConfig` + + """ + + def __init__(self, server, installer, config): self.network = network.Network(server) self.installer = installer + self.config = config def acme_revocation(self, cert): """Handle ACME "revocation" phase. @@ -48,7 +54,7 @@ class Revoker(object): def list_certs_keys(self): """List trusted Let's Encrypt certificates.""" - list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST") + list_file = os.path.join(self.config.CERT_KEY_BACKUP, "LIST") certs = [] if not os.path.isfile(list_file): @@ -69,9 +75,9 @@ class Revoker(object): for row in csvreader: cert = crypto_util.get_cert_info(row[1]) - b_k = os.path.join(CONFIG.CERT_KEY_BACKUP, + b_k = os.path.join(self.config.CERT_KEY_BACKUP, os.path.basename(row[2]) + "_" + row[0]) - b_c = os.path.join(CONFIG.CERT_KEY_BACKUP, + b_c = os.path.join(self.config.CERT_KEY_BACKUP, os.path.basename(row[1]) + "_" + row[0]) cert.update({ @@ -118,8 +124,8 @@ class Revoker(object): :param dict cert: Cert dict used throughout revocation """ - list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST") - list_file2 = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST.tmp") + list_file = os.path.join(self.config.CERT_KEY_BACKUP, "LIST") + list_file2 = os.path.join(self.config.CERT_KEY_BACKUP, "LIST.tmp") with open(list_file, 'rb') as orgfile: csvreader = csv.reader(orgfile) diff --git a/letsencrypt/client/tests/acme_util.py b/letsencrypt/client/tests/acme_util.py index 504009f02..2218de055 100644 --- a/letsencrypt/client/tests/acme_util.py +++ b/letsencrypt/client/tests/acme_util.py @@ -1,5 +1,5 @@ """Class helps construct valid ACME messages for testing.""" -from letsencrypt.client import CONFIG +from letsencrypt.client import constants CHALLENGES = { @@ -62,13 +62,13 @@ CHALLENGES = { def get_dv_challenges(): """Returns all auth challenges.""" return [chall for typ, chall in CHALLENGES.iteritems() - if typ in CONFIG.DV_CHALLENGES] + if typ in constants.DV_CHALLENGES] def get_client_challenges(): """Returns all client challenges.""" return [chall for typ, chall in CHALLENGES.iteritems() - if typ in CONFIG.CLIENT_CHALLENGES] + if typ in constants.CLIENT_CHALLENGES] def get_challenges(): @@ -83,7 +83,7 @@ def gen_combos(challs): combos = [] for i, chall in enumerate(challs): - if chall["type"] in CONFIG.DV_CHALLENGES: + if chall["type"] in constants.DV_CHALLENGES: dv_chall.append(i) else: renewal_chall.append(i) diff --git a/letsencrypt/client/tests/apache/dvsni_test.py b/letsencrypt/client/tests/apache/dvsni_test.py index a50f0a3f6..e831747a8 100644 --- a/letsencrypt/client/tests/apache/dvsni_test.py +++ b/letsencrypt/client/tests/apache/dvsni_test.py @@ -6,8 +6,8 @@ import shutil import mock from letsencrypt.client import challenge_util +from letsencrypt.client import constants from letsencrypt.client import client -from letsencrypt.client import CONFIG from letsencrypt.client.tests.apache import util @@ -157,12 +157,14 @@ class DvsniPerformTest(util.ApacheTest): if vhost.addrs == set(v_addr1): self.assertEqual( vhost.names, - set([str(self.challs[0].nonce + CONFIG.INVALID_EXT)])) + set([str(self.challs[0].nonce + + constants.DVSNI_DOMAIN_SUFFIX)])) else: self.assertEqual(vhost.addrs, set(v_addr2)) self.assertEqual( vhost.names, - set([str(self.challs[1].nonce + CONFIG.INVALID_EXT)])) + set([str(self.challs[1].nonce + + constants.DVSNI_DOMAIN_SUFFIX)])) if __name__ == '__main__': diff --git a/letsencrypt/client/tests/apache/util.py b/letsencrypt/client/tests/apache/util.py index fe27921b7..e84d63b48 100644 --- a/letsencrypt/client/tests/apache/util.py +++ b/letsencrypt/client/tests/apache/util.py @@ -7,7 +7,7 @@ import unittest import mock -from letsencrypt.client import CONFIG +from letsencrypt.client import constants from letsencrypt.client.apache import configurator from letsencrypt.client.apache import obj @@ -50,11 +50,7 @@ def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"): def setup_apache_ssl_options(config_dir): """Move the ssl_options into position and return the path.""" option_path = os.path.join(config_dir, "options-ssl.conf") - temp_options = pkg_resources.resource_filename( - "letsencrypt.client.apache", os.path.basename(CONFIG.OPTIONS_SSL_CONF)) - shutil.copyfile( - temp_options, option_path) - + shutil.copyfile(constants.APACHE_MOD_SSL_CONF, option_path) return option_path @@ -69,7 +65,9 @@ def get_apache_configurator( # This just states that the ssl module is already loaded mock_popen().communicate.return_value = ("ssl_module", "") config = configurator.ApacheConfigurator( - config_path, + mock.MagicMock(APACHE_SERVER_ROOT=config_path, + APACHE_MOD_SSL_CONF=ssl_options, + LE_VHOST_EXT="-le-ssl.conf"), { "backup": backups, "temp": os.path.join(work_dir, "temp_checkpoint"), @@ -77,7 +75,6 @@ def get_apache_configurator( "config": config_dir, "work": work_dir, }, - ssl_options, version) return config diff --git a/letsencrypt/client/tests/challenge_util_test.py b/letsencrypt/client/tests/challenge_util_test.py index 84a561d5d..8e950cc5b 100644 --- a/letsencrypt/client/tests/challenge_util_test.py +++ b/letsencrypt/client/tests/challenge_util_test.py @@ -7,8 +7,8 @@ import unittest import M2Crypto from letsencrypt.client import challenge_util +from letsencrypt.client import constants from letsencrypt.client import client -from letsencrypt.client import CONFIG from letsencrypt.client import le_util @@ -37,11 +37,11 @@ class DvsniGenCertTest(unittest.TestCase): dns_regex = r"DNS:([^, $]*)" cert = M2Crypto.X509.load_cert_string(pem) self.assertEqual( - cert.get_subject().CN, nonce + CONFIG.INVALID_EXT) + cert.get_subject().CN, nonce + constants.DVSNI_DOMAIN_SUFFIX) sans = cert.get_ext("subjectAltName").get_value() - exp_sans = set([nonce + CONFIG.INVALID_EXT, domain, ext]) + exp_sans = set([nonce + constants.DVSNI_DOMAIN_SUFFIX, domain, ext]) act_sans = set(re.findall(dns_regex, sans)) self.assertEqual(exp_sans, act_sans) diff --git a/letsencrypt/client/tests/client_authenticator_test.py b/letsencrypt/client/tests/client_authenticator_test.py index 9c9f4d89f..86c1220e3 100644 --- a/letsencrypt/client/tests/client_authenticator_test.py +++ b/letsencrypt/client/tests/client_authenticator_test.py @@ -6,10 +6,11 @@ import mock class PerformTest(unittest.TestCase): """Test client perform function.""" + def setUp(self): from letsencrypt.client.client_authenticator import ClientAuthenticator - self.auth = ClientAuthenticator("demo_server.org") + self.auth = ClientAuthenticator("demo_server.org", mock.MagicMock()) self.auth.rec_token.perform = mock.MagicMock( name="rec_token_perform", side_effect=gen_client_resp) @@ -45,10 +46,11 @@ class PerformTest(unittest.TestCase): class CleanupTest(unittest.TestCase): """Test the Authenticator cleanup function.""" + def setUp(self): from letsencrypt.client.client_authenticator import ClientAuthenticator - self.auth = ClientAuthenticator("demo_server.org") + self.auth = ClientAuthenticator("demo_server.org", mock.MagicMock()) self.mock_cleanup = mock.MagicMock(name="rec_token_cleanup") self.auth.rec_token.cleanup = self.mock_cleanup diff --git a/letsencrypt/client/tests/client_test.py b/letsencrypt/client/tests/client_test.py index 3c1096b34..df07d1fa9 100644 --- a/letsencrypt/client/tests/client_test.py +++ b/letsencrypt/client/tests/client_test.py @@ -14,7 +14,7 @@ class RollbackTest(unittest.TestCase): @classmethod def _call(cls, checkpoints): from letsencrypt.client.client import rollback - rollback(checkpoints) + rollback(checkpoints, mock.MagicMock()) @mock.patch("letsencrypt.client.client.determine_installer") def test_no_problems(self, mock_det): diff --git a/letsencrypt/client/tests/reverter_test.py b/letsencrypt/client/tests/reverter_test.py index 39ef3d135..0b281b4b8 100644 --- a/letsencrypt/client/tests/reverter_test.py +++ b/letsencrypt/client/tests/reverter_test.py @@ -21,7 +21,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): self.work_dir, self.direc = setup_work_direc() - self.reverter = Reverter(self.direc) + self.reverter = Reverter(mock.MagicMock(), self.direc) tup = setup_test_files() self.config1, self.config2, self.dir1, self.dir2, self.sets = tup @@ -241,7 +241,7 @@ class TestFullCheckpointsReverter(unittest.TestCase): logging.disable(logging.CRITICAL) self.work_dir, self.direc = setup_work_direc() - self.reverter = Reverter(self.direc) + self.reverter = Reverter(mock.MagicMock(), self.direc) tup = setup_test_files() self.config1, self.config2, self.dir1, self.dir2, self.sets = tup @@ -387,14 +387,14 @@ class TestFullCheckpointsReverter(unittest.TestCase): class QuickInitReverterTest(unittest.TestCase): # pylint: disable=too-few-public-methods """Quick test of init.""" + def test_init(self): from letsencrypt.client.reverter import Reverter - rev = Reverter() - - # Verify direc is set - self.assertTrue(rev.direc['backup']) - self.assertTrue(rev.direc['temp']) - self.assertTrue(rev.direc['progress']) + config = mock.MagicMock() + rev = Reverter(config) + self.assertEqual(rev.direc['backup'], config.BACKUP_DIR) + self.assertEqual(rev.direc['temp'], config.TEMP_CHECKPOINT_DIR) + self.assertEqual(rev.direc['progress'], config.IN_PROGRESS_DIR) def setup_work_direc(): diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index b79455c9f..429f13fce 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -82,15 +82,15 @@ def main(): # pylint: disable=too-many-statements,too-many-branches zope.component.provideUtility(displayer) if args.view_config_changes: - client.view_config_changes() + client.view_config_changes(CONFIG) sys.exit() if args.revoke: - client.revoke(args.server) + client.revoke(args.server, CONFIG) sys.exit() if args.rollback > 0: - client.rollback(args.rollback) + client.rollback(args.rollback, CONFIG) sys.exit() if not args.eula: @@ -99,7 +99,7 @@ def main(): # pylint: disable=too-many-statements,too-many-branches # Make sure we actually get an installer that is functioning properly # before we begin to try to use it. try: - installer = client.determine_installer() + installer = client.determine_installer(CONFIG) except errors.LetsEncryptMisconfigurationError as err: logging.fatal("Please fix your configuration before proceeding. " "The Installer exited with the following message: " @@ -110,17 +110,17 @@ def main(): # pylint: disable=too-many-statements,too-many-branches if interfaces.IAuthenticator.providedBy(installer): # pylint: disable=no-member auth = installer else: - auth = client.determine_authenticator() + auth = client.determine_authenticator(CONFIG) domains = choose_names(installer) if args.domains is None else args.domains # Prepare for init of Client if args.privkey is None: - privkey = client.init_key(args.key_size) + privkey = client.init_key(args.key_size, CONFIG.KEY_DIR) else: privkey = client.Client.Key(args.privkey[0], args.privkey[1]) - acme = client.Client(args.server, privkey, auth, installer) + acme = client.Client(args.server, privkey, auth, installer, CONFIG) # Validate the key and csr client.validate_key_csr(privkey) From b6de602b5b8a75e04bc98ebda74451d29b177afa Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Jan 2015 11:28:33 +0000 Subject: [PATCH 02/19] Move CONFIG to CLI arguments --- letsencrypt/client/CONFIG.py | 81 ----------- letsencrypt/client/apache/configurator.py | 48 +++---- letsencrypt/client/augeas_configurator.py | 6 +- letsencrypt/client/client.py | 14 +- letsencrypt/client/client_authenticator.py | 2 +- letsencrypt/client/constants.py | 8 ++ letsencrypt/client/reverter.py | 6 +- letsencrypt/client/revoker.py | 10 +- letsencrypt/client/tests/apache/util.py | 6 +- letsencrypt/client/tests/reverter_test.py | 6 +- letsencrypt/scripts/main.py | 151 +++++++++++++-------- 11 files changed, 152 insertions(+), 186 deletions(-) delete mode 100644 letsencrypt/client/CONFIG.py diff --git a/letsencrypt/client/CONFIG.py b/letsencrypt/client/CONFIG.py deleted file mode 100644 index cc45601b5..000000000 --- a/letsencrypt/client/CONFIG.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Config for Let's Encrypt.""" -import os.path - -import zope.component - -from letsencrypt.client import interfaces - - -zope.component.moduleProvides(interfaces.IConfig) - - -ACME_SERVER = "letsencrypt-demo.org:443" -"""CA hostname (and optionally :port). - -If you create your own server... change this line - -Note: the server certificate must be trusted in order to avoid -further modifications to the client.""" - - -CONFIG_DIR = "/etc/letsencrypt/" -"""Configuration file directory for letsencrypt""" - -WORK_DIR = "/var/lib/letsencrypt/" -"""Working directory for letsencrypt""" - -BACKUP_DIR = os.path.join(WORK_DIR, "backups/") -"""Directory where configuration backups are stored""" - -TEMP_CHECKPOINT_DIR = os.path.join(WORK_DIR, "temp_checkpoint/") -"""Directory where temp checkpoint is created""" - -IN_PROGRESS_DIR = os.path.join(BACKUP_DIR, "IN_PROGRESS/") -"""Directory used before a permanent checkpoint is finalized""" - -CERT_KEY_BACKUP = os.path.join(WORK_DIR, "keys-certs/") -"""Directory where all certificates/keys are stored. Used for easy revocation""" - -REV_TOKENS_DIR = os.path.join(WORK_DIR, "revocation_tokens/") -"""Directory where all revocation tokens are saved.""" - -KEY_DIR = os.path.join(CONFIG_DIR, "keys/") -"""Keys storage.""" - -CERT_DIR = os.path.join(CONFIG_DIR, "certs/") -"""Certificate storage.""" - - -LE_VHOST_EXT = "-le-ssl.conf" -"""Let's Encrypt SSL vhost configuration extension.""" - -CERT_PATH = os.path.join(CERT_DIR, "cert-letsencrypt.pem") -"""Let's Encrypt cert file.""" - -CHAIN_PATH = os.path.join(CERT_DIR, "chain-letsencrypt.pem") -"""Let's Encrypt chain file.""" - - -RSA_KEY_SIZE = 2048 -"""Key size""" - - -APACHE_CTL = "/usr/sbin/apache2ctl" -"""Path to the ``apache2ctl`` binary, used for ``configtest`` and -retrieving Apache2 version number.""" - -APACHE_ENMOD = "apache" -"""Path to the Apache ``a2enmod`` binary.""" - -APACHE_INIT_SCRIPT = "/etc/init.d/apache2" -"""Path to the Apache init script (used for server reload/restart).""" - -APACHE_REWRITE_HTTPS_ARGS = [ - "^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"] -"""Apache rewrite rule arguments used for redirections to https vhost""" - -APACHE_SERVER_ROOT = "/etc/apache2/" -"""Apache server root directory""" - -APACHE_MOD_SSL_CONF = os.path.join(CONFIG_DIR, "options-ssl.conf") -"""Contains standard Apache SSL directives""" diff --git a/letsencrypt/client/apache/configurator.py b/letsencrypt/client/apache/configurator.py index e5b955b27..1f9ecbc0b 100644 --- a/letsencrypt/client/apache/configurator.py +++ b/letsencrypt/client/apache/configurator.py @@ -87,15 +87,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ self.config = config - server_root = self.config.APACHE_SERVER_ROOT - ssl_options = self.config.APACHE_MOD_SSL_CONF + server_root = self.config.apache_server_root + ssl_options = self.config.apache_mod_ssl_conf if direc is None: - direc = {"backup": self.config.BACKUP_DIR, - "temp": self.config.TEMP_CHECKPOINT_DIR, - "progress": self.config.IN_PROGRESS_DIR, - "config": self.config.CONFIG_DIR, - "work": self.config.WORK_DIR} + direc = {"backup": self.config.backup_dir, + "temp": self.config.temp_checkpoint_dir, + "progress": self.config.in_progress_dir, + "config": self.config.config_dir, + "work": self.config.work_dir} super(ApacheConfigurator, self).__init__(config, direc) self.direc = direc @@ -384,10 +384,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): is appropriately listening on port 443. """ - if not mod_loaded("ssl_module", self.config.APACHE_CTL): + if not mod_loaded("ssl_module", self.config.apache_ctl): logging.info("Loading mod_ssl into Apache Server") - enable_mod("ssl", self.config.APACHE_INIT_SCRIPT, - self.config.APACHE_ENMOD) + enable_mod("ssl", self.config.apache_init_script, + self.config.apache_enmod) # Check for Listen 443 # Note: This could be made to also look for ip:443 combo @@ -430,7 +430,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """Makes an ssl_vhost version of a nonssl_vhost. Duplicates vhost and adds default ssl options - New vhost will reside as (nonssl_vhost.path) + ``IConfig.LE_VHOST_EXT`` + New vhost will reside as (nonssl_vhost.path) + ``IConfig.le_vhost_ext`` .. note:: This function saves the configuration @@ -444,9 +444,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): avail_fp = nonssl_vhost.filep # Get filepath of new ssl_vhost if avail_fp.endswith(".conf"): - ssl_fp = avail_fp[:-(len(".conf"))] + self.config.LE_VHOST_EXT + ssl_fp = avail_fp[:-(len(".conf"))] + self.config.le_vhost_ext else: - ssl_fp = avail_fp + self.config.LE_VHOST_EXT + ssl_fp = avail_fp + self.config.le_vhost_ext # First register the creation so that it is properly removed if # configuration is rolled back @@ -566,9 +566,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :rtype: (bool, :class:`letsencrypt.client.apache.obj.VirtualHost`) """ - if not mod_loaded("rewrite_module", self.config.APACHE_CTL): - enable_mod("rewrite", self.config.APACHE_INIT_SCRIPT, - self.config.APACHE_ENMOD) + if not mod_loaded("rewrite_module", self.config.apache_ctl): + enable_mod("rewrite", self.config.apache_init_script, + self.config.apache_enmod) general_v = self._general_vhost(ssl_vhost) if general_v is None: @@ -593,7 +593,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Add directives to server self.parser.add_dir(general_v.path, "RewriteEngine", "On") self.parser.add_dir(general_v.path, "RewriteRule", - self.config.APACHE_REWRITE_HTTPS_ARGS) + constants.APACHE_REWRITE_HTTPS_ARGS) self.save_notes += ('Redirecting host in %s to ssl vhost in %s\n' % (general_v.filep, ssl_vhost.filep)) self.save() @@ -632,10 +632,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if not rewrite_path: # "No existing redirection for virtualhost" return False, -1 - if len(rewrite_path) == len(self.config.APACHE_REWRITE_HTTPS_ARGS): + if len(rewrite_path) == len(constants.APACHE_REWRITE_HTTPS_ARGS): for idx, match in enumerate(rewrite_path): if (self.aug.get(match) != - self.config.APACHE_REWRITE_HTTPS_ARGS[idx]): + constants.APACHE_REWRITE_HTTPS_ARGS[idx]): # Not a letsencrypt https rewrite return True, 2 # Existing letsencrypt https rewrite rule is in place @@ -684,7 +684,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "LogLevel warn\n" "\n" % (servername, serveralias, - " ".join(self.config.APACHE_REWRITE_HTTPS_ARGS))) + " ".join(constants.APACHE_REWRITE_HTTPS_ARGS))) # Write out the file # This is the default name @@ -886,7 +886,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :rtype: bool """ - return apache_restart(self.config.APACHE_INIT_SCRIPT) + return apache_restart(self.config.apache_init_script) def config_test(self): # pylint: disable=no-self-use """Check the configuration of Apache for errors. @@ -897,7 +897,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: proc = subprocess.Popen( - ['sudo', self.config.APACHE_CTL, 'configtest'], # TODO: sudo? + ['sudo', self.config.apache_ctl, 'configtest'], # TODO: sudo? stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() @@ -928,13 +928,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: proc = subprocess.Popen( - [self.config.APACHE_CTL, '-v'], + [self.config.apache_ctl, '-v'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) text = proc.communicate()[0] except (OSError, ValueError): raise errors.LetsEncryptConfiguratorError( - "Unable to run %s -v" % self.config.APACHE_CTL) + "Unable to run %s -v" % self.config.apache_ctl) regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE) matches = regex.findall(text) diff --git a/letsencrypt/client/augeas_configurator.py b/letsencrypt/client/augeas_configurator.py index 57f0feae0..32435dde0 100644 --- a/letsencrypt/client/augeas_configurator.py +++ b/letsencrypt/client/augeas_configurator.py @@ -30,9 +30,9 @@ class AugeasConfigurator(object): """ if not direc: - direc = {"backup": config.BACKUP_DIR, - "temp": config.TEMP_CHECKPOINT_DIR, - "progress": config.IN_PROGRESS_DIR} + direc = {"backup": config.backup_dir, + "temp": config.temp_checkpoint_dir, + "progress": config.in_progress_dir} # Set Augeas flags to not save backup (we do it ourselves) # Set Augeas to not load anything by default diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 9c87a1acb..4765e71ec 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -86,8 +86,8 @@ class Client(object): :rtype: `tuple` of `str` """ - cert_path = self.config.CERT_PATH - chain_path = self.config.CHAIN_PATH + cert_path = self.config.cert_path + chain_path = self.config.chain_path if self.auth_handler is None: logging.warning("Unable to obtain a certificate, because client " @@ -103,7 +103,7 @@ class Client(object): # Create CSR from names if csr is None: - csr = init_csr(self.authkey, domains, self.config.CERT_DIR) + csr = init_csr(self.authkey, domains, self.config.cert_dir) # Retrieve certificate certificate_dict = self.acme_certificate(csr.data) @@ -245,8 +245,8 @@ class Client(object): :rtype: bool """ - list_file = os.path.join(self.config.CERT_KEY_BACKUP, "LIST") - le_util.make_or_verify_dir(self.config.CERT_KEY_BACKUP, 0o700) + list_file = os.path.join(self.config.cert_key_backup, "LIST") + le_util.make_or_verify_dir(self.config.cert_key_backup, 0o700) idx = 0 if encrypt: @@ -271,11 +271,11 @@ class Client(object): shutil.copy2(self.authkey.file, os.path.join( - self.config.CERT_KEY_BACKUP, + self.config.cert_key_backup, os.path.basename(self.authkey.file) + "_" + str(idx))) shutil.copy2(cert_file, os.path.join( - self.config.CERT_KEY_BACKUP, + self.config.cert_key_backup, os.path.basename(cert_file) + "_" + str(idx))) return True diff --git a/letsencrypt/client/client_authenticator.py b/letsencrypt/client/client_authenticator.py index 4a33f719b..d5c0aa5bc 100644 --- a/letsencrypt/client/client_authenticator.py +++ b/letsencrypt/client/client_authenticator.py @@ -28,7 +28,7 @@ class ClientAuthenticator(object): """ self.rec_token = recovery_token.RecoveryToken( - server, config.REV_TOKEN_DIRS) + server, config.rev_token_dirs) def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" diff --git a/letsencrypt/client/constants.py b/letsencrypt/client/constants.py index bf6d6986b..d07737ac7 100644 --- a/letsencrypt/client/constants.py +++ b/letsencrypt/client/constants.py @@ -9,6 +9,7 @@ challanges.""" NONCE_SIZE = 16 """Size of nonce used in JWS objects (in bytes).""" + EXCLUSIVE_CHALLENGES = [frozenset(["dvsni", "simpleHttps"])] """Mutually exclusive challenges.""" @@ -20,6 +21,7 @@ CLIENT_CHALLENGES = frozenset( ["recoveryToken", "recoveryContact", "proofOfPossession"]) """Challenges that are handled by the Let's Encrypt client.""" + ENHANCEMENTS = ["redirect", "http-header", "ocsp-stapling", "spdy"] """List of possible :class:`letsencrypt.client.interfaces.IInstaller` enhancements. @@ -32,10 +34,16 @@ List of expected options parameters: """ + APACHE_MOD_SSL_CONF = pkg_resources.resource_filename( 'letsencrypt.client.apache', 'options-ssl.conf') """Path to the Apache mod_ssl config file found in the Let's Encrypt distribution.""" +APACHE_REWRITE_HTTPS_ARGS = [ + "^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"] +"""Apache rewrite rule arguments used for redirections to https vhost""" + + DVSNI_DOMAIN_SUFFIX = ".acme.invalid" """Suffix appended to domains in DVSNI validation.""" diff --git a/letsencrypt/client/reverter.py b/letsencrypt/client/reverter.py index f60204a76..a67f8f987 100644 --- a/letsencrypt/client/reverter.py +++ b/letsencrypt/client/reverter.py @@ -23,9 +23,9 @@ class Reverter(object): """ if direc is None: - direc = {'backup': config.BACKUP_DIR, - 'temp': config.TEMP_CHECKPOINT_DIR, - 'progress': config.IN_PROGRESS_DIR} + direc = {'backup': config.backup_dir, + 'temp': config.temp_checkpoint_dir, + 'progress': config.in_progress_dir} self.direc = direc def revert_temporary_config(self): diff --git a/letsencrypt/client/revoker.py b/letsencrypt/client/revoker.py index 6093b2dd2..25c912975 100644 --- a/letsencrypt/client/revoker.py +++ b/letsencrypt/client/revoker.py @@ -54,7 +54,7 @@ class Revoker(object): def list_certs_keys(self): """List trusted Let's Encrypt certificates.""" - list_file = os.path.join(self.config.CERT_KEY_BACKUP, "LIST") + list_file = os.path.join(self.config.cert_key_backup, "LIST") certs = [] if not os.path.isfile(list_file): @@ -75,9 +75,9 @@ class Revoker(object): for row in csvreader: cert = crypto_util.get_cert_info(row[1]) - b_k = os.path.join(self.config.CERT_KEY_BACKUP, + b_k = os.path.join(self.config.cert_key_backup, os.path.basename(row[2]) + "_" + row[0]) - b_c = os.path.join(self.config.CERT_KEY_BACKUP, + b_c = os.path.join(self.config.cert_key_backup, os.path.basename(row[1]) + "_" + row[0]) cert.update({ @@ -124,8 +124,8 @@ class Revoker(object): :param dict cert: Cert dict used throughout revocation """ - list_file = os.path.join(self.config.CERT_KEY_BACKUP, "LIST") - list_file2 = os.path.join(self.config.CERT_KEY_BACKUP, "LIST.tmp") + list_file = os.path.join(self.config.cert_key_backup, "LIST") + list_file2 = os.path.join(self.config.cert_key_backup, "LIST.tmp") with open(list_file, 'rb') as orgfile: csvreader = csv.reader(orgfile) diff --git a/letsencrypt/client/tests/apache/util.py b/letsencrypt/client/tests/apache/util.py index e84d63b48..eff556f5d 100644 --- a/letsencrypt/client/tests/apache/util.py +++ b/letsencrypt/client/tests/apache/util.py @@ -65,9 +65,9 @@ def get_apache_configurator( # This just states that the ssl module is already loaded mock_popen().communicate.return_value = ("ssl_module", "") config = configurator.ApacheConfigurator( - mock.MagicMock(APACHE_SERVER_ROOT=config_path, - APACHE_MOD_SSL_CONF=ssl_options, - LE_VHOST_EXT="-le-ssl.conf"), + mock.MagicMock(apache_server_root=config_path, + apache_mod_ssl_conf=ssl_options, + le_vhost_ext="-le-ssl.conf"), { "backup": backups, "temp": os.path.join(work_dir, "temp_checkpoint"), diff --git a/letsencrypt/client/tests/reverter_test.py b/letsencrypt/client/tests/reverter_test.py index 0b281b4b8..1ab4bb70e 100644 --- a/letsencrypt/client/tests/reverter_test.py +++ b/letsencrypt/client/tests/reverter_test.py @@ -392,9 +392,9 @@ class QuickInitReverterTest(unittest.TestCase): from letsencrypt.client.reverter import Reverter config = mock.MagicMock() rev = Reverter(config) - self.assertEqual(rev.direc['backup'], config.BACKUP_DIR) - self.assertEqual(rev.direc['temp'], config.TEMP_CHECKPOINT_DIR) - self.assertEqual(rev.direc['progress'], config.IN_PROGRESS_DIR) + self.assertEqual(rev.direc['backup'], config.backup_dir) + self.assertEqual(rev.direc['temp'], config.temp_checkpoint_dir) + self.assertEqual(rev.direc['progress'], config.in_progress_dir) def setup_work_direc(): diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 429f13fce..e3fe74035 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -13,57 +13,95 @@ import zope.component import zope.interface import letsencrypt -from letsencrypt.client import CONFIG + from letsencrypt.client import client from letsencrypt.client import display from letsencrypt.client import errors from letsencrypt.client import interfaces from letsencrypt.client import log - -def main(): # pylint: disable=too-many-statements,too-many-branches - """Command line argument parsing and main script execution.""" +def create_parser(): + """Create parser.""" parser = argparse.ArgumentParser( description="letsencrypt client %s" % letsencrypt.__version__) - parser.add_argument("-d", "--domains", dest="domains", metavar="DOMAIN", - nargs="+") - parser.add_argument("-s", "--server", dest="server", - default=CONFIG.ACME_SERVER, - help="The ACME CA server. [%(default)s]") - parser.add_argument("-p", "--privkey", dest="privkey", type=read_file, - help="Path to the private key file for certificate " - "generation.") - parser.add_argument("-b", "--rollback", dest="rollback", type=int, - default=0, metavar="N", - help="Revert configuration N number of checkpoints.") - parser.add_argument("-B", "--keysize", dest="key_size", type=int, - default=CONFIG.RSA_KEY_SIZE, metavar="N", - help="RSA key shall be sized N bits. [%(default)d]") - parser.add_argument("-k", "--revoke", dest="revoke", action="store_true", - help="Revoke a certificate.") - parser.add_argument("-v", "--view-config-changes", - dest="view_config_changes", - action="store_true", - help="View checkpoints and associated configuration " - "changes.") - parser.add_argument("-r", "--redirect", dest="redirect", - action="store_const", const=True, - help="Automatically redirect all HTTP traffic to HTTPS " - "for the newly authenticated vhost.") - parser.add_argument("-n", "--no-redirect", dest="redirect", - action="store_const", const=False, - help="Skip the HTTPS redirect question, allowing both " - "HTTP and HTTPS.") - parser.add_argument("-e", "--agree-tos", dest="eula", action="store_true", - help="Skip the end user license agreement screen.") - parser.add_argument("-t", "--text", dest="use_curses", action="store_false", - help="Use the text output instead of the curses UI.") - parser.add_argument("--test", dest="test", action="store_true", - help="Run in test mode.") + add = parser.add_argument + + add("-d", "--domains", metavar="DOMAIN", nargs="+") + add("-s", "--acme-server", "--server", default="letsencrypt-demo.org:443", + help="CA hostname (and optionally :port). The server certificate must " + "be trusted in order to avoid further modifications to the " + "client.") + + add("-p", "--privkey", type=read_file, + help="Path to the private key file for certificate generation.") + add("-B", "--rsa-key-size", "--keysize", type=int, default=2048, + metavar="N", help="RSA key shall be sized N bits.") + + add("-k", "--revoke", action="store_true", help="Revoke a certificate.") + add("-b", "--rollback", type=int, default=0, metavar="N", + help="Revert configuration N number of checkpoints.") + add("-v", "--view-config-changes", action="store_true", + help="View checkpoints and associated configuration changes.") + add("-r", "--redirect", action="store_true", + help="Automatically redirect all HTTP traffic to HTTPS for the newly " + "authenticated vhost.") + + add("-e", "--agree-tos", dest="eula", action="store_true", + help="Skip the end user license agreement screen.") + add("-t", "--text", dest="use_curses", action="store_false", + help="Use the text output instead of the curses UI.") + add("--test", action="store_true", help="Run in test mode.") + + # TODO: trailing slashes might be important! check and remove + add("--config-dir", default="/etc/letsencrypt/", + help="Configuration directory.") + add("--work-dir", default="/var/lib/letsencrypt/", + help="Working directory.") + add("--backup-dir", default="/var/lib/letsencrypt/backups/", + help="Configuration backups directory.") + add("--temp-checkpoint-dir", + default="/var/lib/letsencrypt/temp_checkpoint/", + help="Temporary checkpoint directory.") + add("--in-progress-dir", + default="/var/lib/letsencrypt/backups/IN_PROGRESS/", + help="Directory used before a permanent checkpoint is finalized") + add("--cert-key-backup", default="/var/lib/letsencrypt/keys-certs/", + help="Directory where all certificates and keys are stored. " + "Used for easy revocation.") + add("--rev-tokens-dir", default="/var/lib/letsencrypt/revocation_tokens/", + help="Directory where all revocation tokens are saved.") + add("--key-dir", default="/etc/letsencrypt/keys/", help="Keys storage.") + add("--cert-dir", default="/etc/letsencrypt/certs/", + help="Certificates storage.") + + add("--le-vhost-ext", default="-le-ssl.conf", + help="SSL vhost configuration extension.") + add("--cert-path", default="/etc/letsencrypt/certs/cert-letsencrypt.pem", + help="Let's Encrypt certificate file.") + add("--chain-path", default="/etc/letsencrypt/certs/chain-letsencrypt.pem", + help="Let's Encrypt chain file.") + + add("--apache-ctl", default="/usr/bin/apache2ctl", + help="Path to the 'apache2ctl' binary, used for 'configtest' and " + "retrieving Apache2 version number.") + add("--apache-enmod", default="a2enmod", + help="Path to the Apache 'a2enmod' binary.") + add("--apache-init-script", default="/etc/init.d/apache2", + help="Path to the Apache init script (used for server reload/restart).") + add("--apache-server-root", default="/etc/apache2", + help="Apache server root directory.") + add("--apache-mod-ssl-conf", default="/etc/letsencrypt/options-ssl.conf", + help="Contains standard Apache SSL directives.") + + return parser + +def main(): # pylint: disable=too-many-branches + """Command line argument parsing and main script execution.""" # note: arg parser internally handles --help (and exits afterwards) - args = parser.parse_args() + config = create_parser().parse_args() + zope.interface.directlyProvides(config, interfaces.IConfig) # note: check is done after arg parsing as --help should work w/o root also. if not os.geteuid() == 0: @@ -74,32 +112,32 @@ def main(): # pylint: disable=too-many-statements,too-many-branches # Set up logging logger = logging.getLogger() logger.setLevel(logging.INFO) - if args.use_curses: + if config.use_curses: logger.addHandler(log.DialogHandler()) displayer = display.NcursesDisplay() else: displayer = display.FileDisplay(sys.stdout) zope.component.provideUtility(displayer) - if args.view_config_changes: - client.view_config_changes(CONFIG) + if config.view_config_changes: + client.view_config_changes(config) sys.exit() - if args.revoke: - client.revoke(args.server, CONFIG) + if config.revoke: + client.revoke(config.acme_server, config) sys.exit() - if args.rollback > 0: - client.rollback(args.rollback, CONFIG) + if config.rollback > 0: + client.rollback(config.rollback, config) sys.exit() - if not args.eula: + if not config.eula: display_eula() # Make sure we actually get an installer that is functioning properly # before we begin to try to use it. try: - installer = client.determine_installer(CONFIG) + installer = client.determine_installer(config) except errors.LetsEncryptMisconfigurationError as err: logging.fatal("Please fix your configuration before proceeding. " "The Installer exited with the following message: " @@ -110,17 +148,18 @@ def main(): # pylint: disable=too-many-statements,too-many-branches if interfaces.IAuthenticator.providedBy(installer): # pylint: disable=no-member auth = installer else: - auth = client.determine_authenticator(CONFIG) + auth = client.determine_authenticator(config) - domains = choose_names(installer) if args.domains is None else args.domains + if config.domains is None: + domains = choose_names(installer) # Prepare for init of Client - if args.privkey is None: - privkey = client.init_key(args.key_size, CONFIG.KEY_DIR) + if config.privkey is None: + privkey = client.init_key(config.key_size, config.key_dir) else: - privkey = client.Client.Key(args.privkey[0], args.privkey[1]) + privkey = client.Client.Key(config.privkey[0], config.privkey[1]) - acme = client.Client(args.server, privkey, auth, installer, CONFIG) + acme = client.Client(config.acme_server, privkey, auth, installer, config) # Validate the key and csr client.validate_key_csr(privkey) @@ -134,7 +173,7 @@ def main(): # pylint: disable=too-many-statements,too-many-branches if installer is not None and cert_file is not None: acme.deploy_certificate(domains, privkey, cert_file, chain_file) if installer is not None: - acme.enhance_config(domains, args.redirect) + acme.enhance_config(domains, config.redirect) def display_eula(): From 098f779a7966aa5fe18311ec5961d297e4bdfea2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Jan 2015 11:37:24 +0000 Subject: [PATCH 03/19] Use ConfArgParse --- letsencrypt/scripts/main.py | 3 ++- setup.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index e3fe74035..e0c2d4ce8 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -9,6 +9,7 @@ import logging import os import sys +import confargparse import zope.component import zope.interface @@ -22,7 +23,7 @@ from letsencrypt.client import log def create_parser(): """Create parser.""" - parser = argparse.ArgumentParser( + parser = confargparse.ConfArgParser( description="letsencrypt client %s" % letsencrypt.__version__) add = parser.add_argument diff --git a/setup.py b/setup.py index 5501c7dd6..0d4f58922 100755 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ changes = read_file(os.path.join(here, 'CHANGES.rst')) install_requires = [ 'argparse', + 'ConfArgParse', 'jsonschema', 'mock', 'pycrypto', From 0f22318c2ff620a0fa70da097e42558f812c3300 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Jan 2015 12:32:43 +0000 Subject: [PATCH 04/19] pylint ignore no-member in ConfArgParse --- .pylintrc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.pylintrc b/.pylintrc index 44fc15b1c..9cd5a8781 100644 --- a/.pylintrc +++ b/.pylintrc @@ -239,9 +239,7 @@ ignore-mixin-members=yes # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis - -# Get rid of the spurious no-member errors in pkg_resources -ignored-modules=pkg_resources +ignored-modules=pkg_resources,confargparse,argparse # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). From 787f791a4e67fb32d5843316d12d67ac75109f25 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Jan 2015 14:18:01 +0000 Subject: [PATCH 05/19] --apache-ctl wihout dirname --- letsencrypt/scripts/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index e0c2d4ce8..d50ec362c 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -83,7 +83,7 @@ def create_parser(): add("--chain-path", default="/etc/letsencrypt/certs/chain-letsencrypt.pem", help="Let's Encrypt chain file.") - add("--apache-ctl", default="/usr/bin/apache2ctl", + add("--apache-ctl", default="apache2ctl", help="Path to the 'apache2ctl' binary, used for 'configtest' and " "retrieving Apache2 version number.") add("--apache-enmod", default="a2enmod", From 9580a763e1ac1ac1a0e91bf4838d7a74c031976f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 2 Feb 2015 08:20:55 +0000 Subject: [PATCH 06/19] key_size/keysize -> rsa_key_size --- letsencrypt/scripts/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index d50ec362c..944ae94ef 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -36,7 +36,7 @@ def create_parser(): add("-p", "--privkey", type=read_file, help="Path to the private key file for certificate generation.") - add("-B", "--rsa-key-size", "--keysize", type=int, default=2048, + add("-B", "--rsa-key-size", type=int, default=2048, metavar="N", help="RSA key shall be sized N bits.") add("-k", "--revoke", action="store_true", help="Revoke a certificate.") @@ -156,7 +156,7 @@ def main(): # pylint: disable=too-many-branches # Prepare for init of Client if config.privkey is None: - privkey = client.init_key(config.key_size, config.key_dir) + privkey = client.init_key(config.rsa_key_size, config.key_dir) else: privkey = client.Client.Key(config.privkey[0], config.privkey[1]) From 7828853e8c593ded6e20233580e08ff4ab7ac7fd Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 2 Feb 2015 14:14:32 +0000 Subject: [PATCH 07/19] Remove "direc" magic, use IConfig instead. f(config.x, config) -> f(config) --- letsencrypt/client/apache/configurator.py | 33 +++------ letsencrypt/client/apache/dvsni.py | 39 +++++----- letsencrypt/client/augeas_configurator.py | 22 ++---- letsencrypt/client/client.py | 31 +++----- letsencrypt/client/client_authenticator.py | 6 +- letsencrypt/client/reverter.py | 73 +++++++++---------- letsencrypt/client/revoker.py | 6 +- letsencrypt/client/tests/apache/dvsni_test.py | 13 ++-- letsencrypt/client/tests/apache/util.py | 19 +++-- .../client/tests/client_authenticator_test.py | 6 +- letsencrypt/client/tests/client_test.py | 2 +- letsencrypt/client/tests/reverter_test.py | 65 +++++++---------- letsencrypt/scripts/main.py | 6 +- 13 files changed, 138 insertions(+), 183 deletions(-) diff --git a/letsencrypt/client/apache/configurator.py b/letsencrypt/client/apache/configurator.py index 1f9ecbc0b..6b7e03f24 100644 --- a/letsencrypt/client/apache/configurator.py +++ b/letsencrypt/client/apache/configurator.py @@ -65,9 +65,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :ivar config: Configuration. :type config: :class:`~letsencrypt.client.interfaces.IConfig` - :ivar str server_root: Path to Apache root directory - :ivar dict location: Path to various files associated - with the configuration :ivar tup version: version of Apache :ivar list vhosts: All vhosts found in the configuration (:class:`list` of :class:`letsencrypt.client.apache.obj.VirtualHost`) @@ -77,34 +74,22 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ zope.interface.implements(interfaces.IAuthenticator, interfaces.IInstaller) - def __init__(self, config, direc=None, version=None): + def __init__(self, config, version=None): """Initialize an Apache Configurator. - :param dict direc: locations of various config directories - (used mostly for unittesting) :param tup version: version of Apache as a tuple (2, 4, 7) (used mostly for unittesting) """ - self.config = config - server_root = self.config.apache_server_root - ssl_options = self.config.apache_mod_ssl_conf - - if direc is None: - direc = {"backup": self.config.backup_dir, - "temp": self.config.temp_checkpoint_dir, - "progress": self.config.in_progress_dir, - "config": self.config.config_dir, - "work": self.config.work_dir} - - super(ApacheConfigurator, self).__init__(config, direc) - self.direc = direc + super(ApacheConfigurator, self).__init__(config) # Verify that all directories and files exist with proper permissions if os.geteuid() == 0: self.verify_setup() - self.parser = parser.ApacheParser(self.aug, server_root, ssl_options) + self.parser = parser.ApacheParser( + self.aug, self.config.apache_server_root, + self.config.apache_mod_ssl_conf) # Check for errors in parsing files with Augeas self.check_parsing_errors("httpd.aug") @@ -126,7 +111,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self._prepare_server_https() self.enhance_func = {"redirect": self._enable_redirect} - temp_install(ssl_options) + temp_install(self.config.apache_mod_ssl_conf) def deploy_cert(self, domain, cert, key, cert_chain=None): """Deploys certificate to specified virtual host. @@ -954,9 +939,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ uid = os.geteuid() - le_util.make_or_verify_dir(self.direc["config"], 0o755, uid) - le_util.make_or_verify_dir(self.direc["work"], 0o755, uid) - le_util.make_or_verify_dir(self.direc["backup"], 0o755, uid) + le_util.make_or_verify_dir(self.config.config_dir, 0o755, uid) + le_util.make_or_verify_dir(self.config.work_dir, 0o755, uid) + le_util.make_or_verify_dir(self.config.backup_dir, 0o755, uid) ########################################################################### # Challenges Section diff --git a/letsencrypt/client/apache/dvsni.py b/letsencrypt/client/apache/dvsni.py index 242c0d324..79ea5f5c4 100644 --- a/letsencrypt/client/apache/dvsni.py +++ b/letsencrypt/client/apache/dvsni.py @@ -11,8 +11,8 @@ from letsencrypt.client.apache import parser class ApacheDvsni(object): """Class performs DVSNI challenges within the Apache configurator. - :ivar config: ApacheConfigurator object - :type config: + :ivar configurator: ApacheConfigurator object + :type configurator: :class:`letsencrypt.client.apache.configurator.ApacheConfigurator` :ivar dvsni_chall: Data required for challenges. @@ -33,12 +33,12 @@ class ApacheDvsni(object): :param str challenge_conf: location of the challenge config file """ - def __init__(self, config): - self.config = config + def __init__(self, configurator): + self.configurator = configurator self.dvsni_chall = [] self.indices = [] self.challenge_conf = os.path.join( - config.direc["config"], "le_dvsni_cert_challenge.conf") + configurator.config.config_dir, "le_dvsni_cert_challenge.conf") # self.completed = 0 def add_chall(self, chall, idx=None): @@ -60,12 +60,12 @@ class ApacheDvsni(object): return None # Save any changes to the configuration as a precaution # About to make temporary changes to the config - self.config.save() + self.configurator.save() addresses = [] default_addr = "*:443" for chall in self.dvsni_chall: - vhost = self.config.choose_vhost(chall.domain) + vhost = self.configurator.choose_vhost(chall.domain) if vhost is None: logging.error( "No vhost exists with servername or alias of: %s", @@ -75,7 +75,7 @@ class ApacheDvsni(object): return None # TODO - @jdkasten review this code to make sure it makes sense - self.config.make_server_sni_ready(vhost, default_addr) + self.configurator.make_server_sni_ready(vhost, default_addr) for addr in vhost.addrs: if "_default_" == addr.get_addr(): @@ -95,7 +95,7 @@ class ApacheDvsni(object): self._mod_config(addresses) # Save reversible changes - self.config.save("SNI Challenge", True) + self.configurator.save("SNI Challenge", True) return responses @@ -103,7 +103,7 @@ class ApacheDvsni(object): """Generate and write out challenge certificate.""" cert_path = self.get_cert_file(chall.nonce) # Register the path before you write out the file - self.config.reverter.register_file_creation(True, cert_path) + self.configurator.reverter.register_file_creation(True, cert_path) cert_pem, s_b64 = challenge_util.dvsni_gen_cert( chall.domain, chall.r_b64, chall.nonce, chall.key) @@ -131,8 +131,9 @@ class ApacheDvsni(object): self.dvsni_chall[idx].key.file) config_text += "\n" - self._conf_include_check(self.config.parser.loc["default"]) - self.config.reverter.register_file_creation(True, self.challenge_conf) + self._conf_include_check(self.configurator.parser.loc["default"]) + self.configurator.reverter.register_file_creation( + True, self.challenge_conf) with open(self.challenge_conf, 'w') as new_conf: new_conf.write(config_text) @@ -146,11 +147,12 @@ class ApacheDvsni(object): :param str main_config: file path to main user apache config file """ - if len(self.config.parser.find_dir( + if len(self.configurator.parser.find_dir( parser.case_i("Include"), self.challenge_conf)) == 0: # print "Including challenge virtual host(s)" - self.config.parser.add_dir(parser.get_aug_path(main_config), - "Include", self.challenge_conf) + self.configurator.parser.add_dir( + parser.get_aug_path(main_config), + "Include", self.challenge_conf) def _get_config_text(self, nonce, ip_addrs, dvsni_key_file): """Chocolate virtual server configuration text @@ -172,11 +174,12 @@ class ApacheDvsni(object): "\n" "LimitRequestBody 1048576\n" "\n" - "Include " + self.config.parser.loc["ssl_options"] + "\n" + "Include " + self.configurator.parser.loc["ssl_options"] + "\n" "SSLCertificateFile " + self.get_cert_file(nonce) + "\n" "SSLCertificateKeyFile " + dvsni_key_file + "\n" "\n" - "DocumentRoot " + self.config.direc["config"] + "dvsni_page/\n" + "DocumentRoot " + + self.configurator.config.config_dir + "dvsni_page/\n" "\n\n") def get_cert_file(self, nonce): @@ -188,4 +191,4 @@ class ApacheDvsni(object): :rtype: str """ - return self.config.direc["work"] + nonce + ".crt" + return self.configurator.config.work_dir + nonce + ".crt" diff --git a/letsencrypt/client/augeas_configurator.py b/letsencrypt/client/augeas_configurator.py index 32435dde0..8854fef09 100644 --- a/letsencrypt/client/augeas_configurator.py +++ b/letsencrypt/client/augeas_configurator.py @@ -9,30 +9,20 @@ from letsencrypt.client import reverter class AugeasConfigurator(object): """Base Augeas Configurator class. + :ivar config: Configuration. + :type config: :class:`~letsencrypt.client.interfaces.IConfig` + :ivar aug: Augeas object :type aug: :class:`augeas.Augeas` :ivar str save_notes: Human-readable configuration change notes - :ivar dict direc: dictionary containing save directory paths :ivar reverter: saves and reverts checkpoints :type reverter: :class:`letsencrypt.client.reverter.Reverter` """ - def __init__(self, config, direc=None): - """Initialize Augeas Configurator. - - :param config: Configuration. - :type config: :class:`~letsencrypt.client.interfaces.IConfig` - - :param dict direc: location of save directories - (used mostly for testing) - - """ - if not direc: - direc = {"backup": config.backup_dir, - "temp": config.temp_checkpoint_dir, - "progress": config.in_progress_dir} + def __init__(self, config): + self.config = config # Set Augeas flags to not save backup (we do it ourselves) # Set Augeas to not load anything by default @@ -44,7 +34,7 @@ class AugeasConfigurator(object): # This needs to occur before VirtualHost objects are setup... # because this will change the underlying configuration and potential # vhosts - self.reverter = reverter.Reverter(config, direc) + self.reverter = reverter.Reverter(config) self.reverter.recovery_routine() def check_parsing_errors(self, lens): diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 4765e71ec..6cc1ad81c 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -49,16 +49,15 @@ class Client(object): # Note: form is the type of data, "pem" or "der" CSR = collections.namedtuple("CSR", "file data form") - def __init__(self, server, authkey, dv_auth, installer, config): + def __init__(self, config, authkey, dv_auth, installer): """Initialize a client. - :param str server: CA server to contact :param dv_auth: IAuthenticator that can solve the :const:`letsencrypt.client.constants.DV_CHALLENGES` :type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator` """ - self.network = network.Network(server) + self.network = network.Network(config.server) self.authkey = authkey self.installer = installer @@ -66,8 +65,7 @@ class Client(object): self.config = config if dv_auth is not None: - client_auth = client_authenticator.ClientAuthenticator( - server, config) + client_auth = client_authenticator.ClientAuthenticator(config) self.auth_handler = auth_handler.AuthHandler( dv_auth, client_auth, self.network) else: @@ -86,9 +84,6 @@ class Client(object): :rtype: `tuple` of `str` """ - cert_path = self.config.cert_path - chain_path = self.config.chain_path - if self.auth_handler is None: logging.warning("Unable to obtain a certificate, because client " "does not have a valid auth handler.") @@ -110,7 +105,7 @@ class Client(object): # Save Certificate cert_file, chain_file = self.save_certificate( - certificate_dict, cert_path, chain_path) + certificate_dict, self.config.cert_path, self.config.chain_path) self.store_cert_key(cert_file, False) @@ -426,7 +421,7 @@ def determine_installer(config): logging.info("Unable to find a way to install the certificate.") -def rollback(checkpoints, config): +def rollback(config): """Revert configuration the specified number of checkpoints. .. note:: If another installer uses something other than the reverter class @@ -441,8 +436,6 @@ def rollback(checkpoints, config): of future installers. Perhaps the interface should define errors that are thrown for the various functions. - :param int checkpoints: Number of checkpoints to revert. - :param config: Configuration. :type config: :class:`letsencrypt.client.interfaces.IConfig` @@ -451,18 +444,18 @@ def rollback(checkpoints, config): try: installer = determine_installer(config) except errors.LetsEncryptMisconfigurationError: - _misconfigured_rollback(checkpoints, config) + _misconfigured_rollback(config) return # No Errors occurred during init... proceed normally # If installer is None... couldn't find an installer... there shouldn't be # anything to rollback if installer is not None: - installer.rollback_checkpoints(checkpoints) + installer.rollback_checkpoints(config.rollback) installer.restart() -def _misconfigured_rollback(checkpoints, config): +def _misconfigured_rollback(config): """Handles the case where the Installer is misconfigured. :param config: Configuration. @@ -484,7 +477,7 @@ def _misconfigured_rollback(checkpoints, config): # Also... not sure how future installers will handle recovery. rev = reverter.Reverter(config) rev.recovery_routine() - rev.rollback_checkpoints(checkpoints) + rev.rollback_checkpoints(config.rollback) # We should try to restart the server try: @@ -497,11 +490,9 @@ def _misconfigured_rollback(checkpoints, config): "Rollback was unable to solve the misconfiguration issues") -def revoke(server, config): +def revoke(config): """Revoke certificates. - :param str server: ACME server the client wishes to revoke certificates from - :param config: Configuration. :type config: :class:`letsencrypt.client.interfaces.IConfig` @@ -525,7 +516,7 @@ def revoke(server, config): "revocation without a valid installer. This feature should come " "soon.") return - revoc = revoker.Revoker(server, installer, config) + revoc = revoker.Revoker(installer, config) revoc.list_certs_keys() diff --git a/letsencrypt/client/client_authenticator.py b/letsencrypt/client/client_authenticator.py index d5c0aa5bc..2897a5132 100644 --- a/letsencrypt/client/client_authenticator.py +++ b/letsencrypt/client/client_authenticator.py @@ -18,17 +18,15 @@ class ClientAuthenticator(object): zope.interface.implements(interfaces.IAuthenticator) # This will have an installer soon for get_key/cert purposes - def __init__(self, server, config): + def __init__(self, config): """Initialize Client Authenticator. - :param str server: ACME CA Server - :param config: Configuration. :type config: :class:`letsencrypt.client.interfaces.IConfig` """ self.rec_token = recovery_token.RecoveryToken( - server, config.rev_token_dirs) + config.acme_server, config.rev_token_dirs) def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" diff --git a/letsencrypt/client/reverter.py b/letsencrypt/client/reverter.py index a67f8f987..3f008fc38 100644 --- a/letsencrypt/client/reverter.py +++ b/letsencrypt/client/reverter.py @@ -13,20 +13,14 @@ from letsencrypt.client import le_util class Reverter(object): - """Reverter Class - save and revert configuration checkpoints.""" + """Reverter Class - save and revert configuration checkpoints. - def __init__(self, config, direc=None): - """Initialize Reverter. + :param config: Configuration. + :type config: :class:`letsencrypt.client.interfaces.IConfig` - :param config: Configuration. - :type config: :class:`letsencrypt.client.interfaces.IConfig` - - """ - if direc is None: - direc = {'backup': config.backup_dir, - 'temp': config.temp_checkpoint_dir, - 'progress': config.in_progress_dir} - self.direc = direc + """ + def __init__(self, config): + self.config = config def revert_temporary_config(self): """Reload users original configuration files after a temporary save. @@ -38,13 +32,13 @@ class Reverter(object): Unable to revert config """ - if os.path.isdir(self.direc['temp']): + if os.path.isdir(self.config.temp_checkpoint_dir): try: - self._recover_checkpoint(self.direc['temp']) + self._recover_checkpoint(self.config.temp_checkpoint_dir) except errors.LetsEncryptReverterError: # We have a partial or incomplete recovery logging.fatal("Incomplete or failed recovery for %s", - self.direc['temp']) + self.config.temp_checkpoint_dir) raise errors.LetsEncryptReverterError( "Unable to revert temporary config") @@ -69,7 +63,7 @@ class Reverter(object): logging.error("Rollback argument must be a positive integer") raise errors.LetsEncryptReverterError("Invalid Input") - backups = os.listdir(self.direc['backup']) + backups = os.listdir(self.config.backup_dir) backups.sort() if len(backups) < rollback: @@ -77,7 +71,7 @@ class Reverter(object): rollback, len(backups)) while rollback > 0 and backups: - cp_dir = os.path.join(self.direc['backup'], backups.pop()) + cp_dir = os.path.join(self.config.backup_dir, backups.pop()) try: self._recover_checkpoint(cp_dir) except errors.LetsEncryptReverterError: @@ -94,7 +88,7 @@ class Reverter(object): .. todo:: Decide on a policy for error handling, OSError IOError... """ - backups = os.listdir(self.direc['backup']) + backups = os.listdir(self.config.backup_dir) backups.sort(reverse=True) if not backups: @@ -108,12 +102,12 @@ class Reverter(object): float(bkup) except ValueError: raise errors.LetsEncryptReverterError( - "Invalid directories in {0}".format(self.direc['backup'])) + "Invalid directories in {0}".format(self.config.backup_dir)) output = [] for bkup in backups: output.append(time.ctime(float(bkup))) - cur_dir = os.path.join(self.direc['backup'], bkup) + cur_dir = os.path.join(self.config.backup_dir, bkup) with open(os.path.join(cur_dir, "CHANGES_SINCE")) as changes_fd: output.append(changes_fd.read()) @@ -142,7 +136,8 @@ class Reverter(object): param str save_notes: notes about changes during the save """ - self._add_to_checkpoint_dir(self.direc['temp'], save_files, save_notes) + self._add_to_checkpoint_dir( + self.config.temp_checkpoint_dir, save_files, save_notes) def add_to_checkpoint(self, save_files, save_notes): """Add files to a permanent checkpoint @@ -154,7 +149,7 @@ class Reverter(object): # Check to make sure we are not overwriting a temp file self._check_tempfile_saves(save_files) self._add_to_checkpoint_dir( - self.direc['progress'], save_files, save_notes) + self.config.in_progress_dir, save_files, save_notes) def _add_to_checkpoint_dir(self, cp_dir, save_files, save_notes): """Add save files to checkpoint directory. @@ -264,13 +259,13 @@ class Reverter(object): protected_files = [] # Get temp modified files - temp_path = os.path.join(self.direc['temp'], "FILEPATHS") + temp_path = os.path.join(self.config.temp_checkpoint_dir, "FILEPATHS") if os.path.isfile(temp_path): with open(temp_path, 'r') as protected_fd: protected_files.extend(protected_fd.read().splitlines()) # Get temp new files - new_path = os.path.join(self.direc['temp'], "NEW_FILES") + new_path = os.path.join(self.config.temp_checkpoint_dir, "NEW_FILES") if os.path.isfile(new_path): with open(new_path, 'r') as protected_fd: protected_files.extend(protected_fd.read().splitlines()) @@ -305,9 +300,9 @@ class Reverter(object): "Forgot to provide files to registration call") if temporary: - cp_dir = self.direc['temp'] + cp_dir = self.config.temp_checkpoint_dir else: - cp_dir = self.direc['progress'] + cp_dir = self.config.in_progress_dir le_util.make_or_verify_dir(cp_dir, 0o755, os.geteuid()) @@ -331,7 +326,7 @@ class Reverter(object): def recovery_routine(self): """Revert all previously modified files. - First, any changes found in self.direc['temp'] are removed, + First, any changes found in IConfig.temp_checkpoint_dir are removed, then IN_PROGRESS changes are removed The order is important. IN_PROGRESS is unable to add files that are already added by a TEMP change. Thus TEMP must be rolled back first because that will be the @@ -339,17 +334,17 @@ class Reverter(object): """ self.revert_temporary_config() - if os.path.isdir(self.direc['progress']): + if os.path.isdir(self.config.in_progress_dir): try: - self._recover_checkpoint(self.direc['progress']) + self._recover_checkpoint(self.config.in_progress_dir) except errors.LetsEncryptReverterError: # We have a partial or incomplete recovery logging.fatal("Incomplete or failed recovery for IN_PROGRESS " "checkpoint - %s", - self.direc['progress']) + self.config.in_progress_dir) raise errors.LetsEncryptReverterError( "Incomplete or failed recovery for IN_PROGRESS checkpoint " - "- %s" % self.direc['progress']) + "- %s" % self.config.in_progress_dir) def _remove_contained_files(self, file_list): # pylint: disable=no-self-use """Erase all files contained within file_list. @@ -392,8 +387,8 @@ class Reverter(object): def finalize_checkpoint(self, title): """Move IN_PROGRESS checkpoint to timestamped checkpoint. - Adds title to self.direc['progress'] CHANGES_SINCE - Move self.direc['progress'] to Backups directory and + Adds title to self.config.in_progress_dir CHANGES_SINCE + Move self.config.in_progress_dir to Backups directory and rename the directory as a timestamp :param str title: Title describing checkpoint @@ -402,14 +397,14 @@ class Reverter(object): """ # Check to make sure an "in progress" directory exists - if not os.path.isdir(self.direc['progress']): + if not os.path.isdir(self.config.in_progress_dir): logging.warning("No IN_PROGRESS checkpoint to finalize") return changes_since_path = os.path.join( - self.direc['progress'], 'CHANGES_SINCE') + self.config.in_progress_dir, 'CHANGES_SINCE') changes_since_tmp_path = os.path.join( - self.direc['progress'], 'CHANGES_SINCE.tmp') + self.config.in_progress_dir, 'CHANGES_SINCE.tmp') try: with open(changes_since_tmp_path, 'w') as changes_tmp: @@ -430,9 +425,9 @@ class Reverter(object): # collisions in the naming convention. cur_time = time.time() for _ in range(10): - final_dir = os.path.join(self.direc['backup'], str(cur_time)) + final_dir = os.path.join(self.config.backup_dir, str(cur_time)) try: - os.rename(self.direc['progress'], final_dir) + os.rename(self.config.in_progress_dir, final_dir) return except OSError: # It is possible if the checkpoints are made extremely quickly @@ -443,6 +438,6 @@ class Reverter(object): # After 10 attempts... something is probably wrong here... logging.error( "Unable to finalize checkpoint, %s -> %s", - self.direc['progress'], final_dir) + self.config.in_progress_dir, final_dir) raise errors.LetsEncryptReverterError( "Unable to finalize checkpoint renaming") diff --git a/letsencrypt/client/revoker.py b/letsencrypt/client/revoker.py index 25c912975..3a194610b 100644 --- a/letsencrypt/client/revoker.py +++ b/letsencrypt/client/revoker.py @@ -17,13 +17,13 @@ from letsencrypt.client import network class Revoker(object): """A revocation class for LE. - :param config: Configuration. + :ivar config: Configuration. :type config: :class:`letsencrypt.client.interfaces.IConfig` """ - def __init__(self, server, installer, config): - self.network = network.Network(server) + def __init__(self, installer, config): + self.network = network.Network(config.acme_server) self.installer = installer self.config = config diff --git a/letsencrypt/client/tests/apache/dvsni_test.py b/letsencrypt/client/tests/apache/dvsni_test.py index e831747a8..848652f67 100644 --- a/letsencrypt/client/tests/apache/dvsni_test.py +++ b/letsencrypt/client/tests/apache/dvsni_test.py @@ -100,7 +100,7 @@ class DvsniPerformTest(util.ApacheTest): # Check to make sure challenge config path is included in apache config. self.assertEqual( - len(self.sni.config.parser.find_dir( + len(self.sni.configurator.parser.find_dir( "Include", self.sni.challenge_conf)), 1) self.assertEqual(len(responses), 1) @@ -125,7 +125,7 @@ class DvsniPerformTest(util.ApacheTest): mock_setup_cert.call_args_list[1], mock.call(self.challs[1])) self.assertEqual( - len(self.sni.config.parser.find_dir( + len(self.sni.configurator.parser.find_dir( "Include", self.sni.challenge_conf)), 1) self.assertEqual(len(responses), 2) @@ -142,16 +142,17 @@ class DvsniPerformTest(util.ApacheTest): ll_addr.append(v_addr1) ll_addr.append(v_addr2) self.sni._mod_config(ll_addr) # pylint: disable=protected-access - self.sni.config.save() + self.sni.configurator.save() - self.sni.config.parser.find_dir("Include", self.sni.challenge_conf) - vh_match = self.sni.config.aug.match( + self.sni.configurator.parser.find_dir( + "Include", self.sni.challenge_conf) + vh_match = self.sni.configurator.aug.match( "/files" + self.sni.challenge_conf + "//VirtualHost") vhs = [] for match in vh_match: # pylint: disable=protected-access - vhs.append(self.sni.config._create_vhost(match)) + vhs.append(self.sni.configurator._create_vhost(match)) self.assertEqual(len(vhs), 2) for vhost in vhs: if vhost.addrs == set(v_addr1): diff --git a/letsencrypt/client/tests/apache/util.py b/letsencrypt/client/tests/apache/util.py index eff556f5d..df981aede 100644 --- a/letsencrypt/client/tests/apache/util.py +++ b/letsencrypt/client/tests/apache/util.py @@ -65,16 +65,15 @@ def get_apache_configurator( # This just states that the ssl module is already loaded mock_popen().communicate.return_value = ("ssl_module", "") config = configurator.ApacheConfigurator( - mock.MagicMock(apache_server_root=config_path, - apache_mod_ssl_conf=ssl_options, - le_vhost_ext="-le-ssl.conf"), - { - "backup": backups, - "temp": os.path.join(work_dir, "temp_checkpoint"), - "progress": os.path.join(backups, "IN_PROGRESS"), - "config": config_dir, - "work": work_dir, - }, + mock.MagicMock( + apache_server_root=config_path, + apache_mod_ssl_conf=ssl_options, + le_vhost_ext="-le-ssl.conf", + backup_dir=backups, + config_dir=config_dir, + temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), + in_progress_dir=os.path.join(backups, "IN_PROGRESS"), + work_dir=work_dir), version) return config diff --git a/letsencrypt/client/tests/client_authenticator_test.py b/letsencrypt/client/tests/client_authenticator_test.py index 86c1220e3..2f49b07d7 100644 --- a/letsencrypt/client/tests/client_authenticator_test.py +++ b/letsencrypt/client/tests/client_authenticator_test.py @@ -10,7 +10,8 @@ class PerformTest(unittest.TestCase): def setUp(self): from letsencrypt.client.client_authenticator import ClientAuthenticator - self.auth = ClientAuthenticator("demo_server.org", mock.MagicMock()) + self.auth = ClientAuthenticator( + mock.MagicMock(acme_server="demo_server.org")) self.auth.rec_token.perform = mock.MagicMock( name="rec_token_perform", side_effect=gen_client_resp) @@ -50,7 +51,8 @@ class CleanupTest(unittest.TestCase): def setUp(self): from letsencrypt.client.client_authenticator import ClientAuthenticator - self.auth = ClientAuthenticator("demo_server.org", mock.MagicMock()) + self.auth = ClientAuthenticator(mock.MagicMock( + acme_server="demo_server.org")) self.mock_cleanup = mock.MagicMock(name="rec_token_cleanup") self.auth.rec_token.cleanup = self.mock_cleanup diff --git a/letsencrypt/client/tests/client_test.py b/letsencrypt/client/tests/client_test.py index df07d1fa9..6b9235e11 100644 --- a/letsencrypt/client/tests/client_test.py +++ b/letsencrypt/client/tests/client_test.py @@ -14,7 +14,7 @@ class RollbackTest(unittest.TestCase): @classmethod def _call(cls, checkpoints): from letsencrypt.client.client import rollback - rollback(checkpoints, mock.MagicMock()) + rollback(mock.MagicMock(rollback=checkpoints)) @mock.patch("letsencrypt.client.client.determine_installer") def test_no_problems(self, mock_det): diff --git a/letsencrypt/client/tests/reverter_test.py b/letsencrypt/client/tests/reverter_test.py index 1ab4bb70e..d60aa91a6 100644 --- a/letsencrypt/client/tests/reverter_test.py +++ b/letsencrypt/client/tests/reverter_test.py @@ -19,15 +19,14 @@ class ReverterCheckpointLocalTest(unittest.TestCase): # Disable spurious errors... we are trying to test for them logging.disable(logging.CRITICAL) - self.work_dir, self.direc = setup_work_direc() - - self.reverter = Reverter(mock.MagicMock(), self.direc) + self.config = setup_work_direc() + self.reverter = Reverter(self.config) tup = setup_test_files() self.config1, self.config2, self.dir1, self.dir2, self.sets = tup def tearDown(self): - shutil.rmtree(self.work_dir) + shutil.rmtree(self.config.work_dir) shutil.rmtree(self.dir1) shutil.rmtree(self.dir2) @@ -38,13 +37,14 @@ class ReverterCheckpointLocalTest(unittest.TestCase): self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") self.reverter.add_to_temp_checkpoint(self.sets[1], "save2") - self.assertTrue(os.path.isdir(self.reverter.direc['temp'])) - self.assertEqual(get_save_notes(self.direc['temp']), "save1save2") + self.assertTrue(os.path.isdir(self.config.temp_checkpoint_dir)) + self.assertEqual(get_save_notes( + self.config.temp_checkpoint_dir), "save1save2") self.assertFalse(os.path.isfile( - os.path.join(self.direc['temp'], "NEW_FILES"))) + os.path.join(self.config.temp_checkpoint_dir, "NEW_FILES"))) self.assertEqual( - get_filepaths(self.direc['temp']), + get_filepaths(self.config.temp_checkpoint_dir), "{0}\n{1}\n".format(self.config1, self.config2)) def test_add_to_checkpoint_copy_failure(self): @@ -113,7 +113,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): self.reverter.register_file_creation(True, self.config1) self.reverter.register_file_creation(True, self.config1) - files = get_new_files(self.direc['temp']) + files = get_new_files(self.config.temp_checkpoint_dir) self.assertEqual(len(files), 1) @@ -240,14 +240,14 @@ class TestFullCheckpointsReverter(unittest.TestCase): # Disable spurious errors... logging.disable(logging.CRITICAL) - self.work_dir, self.direc = setup_work_direc() - self.reverter = Reverter(mock.MagicMock(), self.direc) + self.config = setup_work_direc() + self.reverter = Reverter(self.config) tup = setup_test_files() self.config1, self.config2, self.dir1, self.dir2, self.sets = tup def tearDown(self): - shutil.rmtree(self.work_dir) + shutil.rmtree(self.config.work_dir) shutil.rmtree(self.dir1) shutil.rmtree(self.dir2) @@ -269,7 +269,7 @@ class TestFullCheckpointsReverter(unittest.TestCase): config3 = self._setup_three_checkpoints() # Check resulting backup directory - self.assertEqual(len(os.listdir(self.direc['backup'])), 3) + self.assertEqual(len(os.listdir(self.config.backup_dir)), 3) # Check rollbacks # First rollback self.reverter.rollback_checkpoints(1) @@ -285,11 +285,11 @@ class TestFullCheckpointsReverter(unittest.TestCase): self.assertFalse(os.path.isfile(config3)) # One dir left... check title - all_dirs = os.listdir(self.direc['backup']) + all_dirs = os.listdir(self.config.backup_dir) self.assertEqual(len(all_dirs), 1) self.assertTrue( "First Checkpoint" in get_save_notes( - os.path.join(self.direc['backup'], all_dirs[0]))) + os.path.join(self.config.backup_dir, all_dirs[0]))) # Final rollback self.reverter.rollback_checkpoints(1) self.assertEqual(read_in(self.config1), "directive-dir1") @@ -350,7 +350,7 @@ class TestFullCheckpointsReverter(unittest.TestCase): def test_view_config_changes_bad_backups_dir(self): # There shouldn't be any "in progess directories when this is called # It must just be clean checkpoints - os.makedirs(os.path.join(self.direc['backup'], "in_progress")) + os.makedirs(os.path.join(self.config.backup_dir, "in_progress")) self.assertRaises(errors.LetsEncryptReverterError, self.reverter.view_config_changes) @@ -384,29 +384,20 @@ class TestFullCheckpointsReverter(unittest.TestCase): return config3 -class QuickInitReverterTest(unittest.TestCase): - # pylint: disable=too-few-public-methods - """Quick test of init.""" - - def test_init(self): - from letsencrypt.client.reverter import Reverter - config = mock.MagicMock() - rev = Reverter(config) - self.assertEqual(rev.direc['backup'], config.backup_dir) - self.assertEqual(rev.direc['temp'], config.temp_checkpoint_dir) - self.assertEqual(rev.direc['progress'], config.in_progress_dir) - - def setup_work_direc(): - """Setup directories.""" - work_dir = tempfile.mkdtemp("work") - backup = os.path.join(work_dir, "backup") - os.makedirs(backup) - direc = {'backup': backup, - 'temp': os.path.join(work_dir, "temp"), - 'progress': os.path.join(backup, "progress")} + """Setup directories. - return work_dir, direc + :returns: Mocked :class:`letsencrypt.client.interfaces.IConfig` + + """ + work_dir = tempfile.mkdtemp("work") + backup_dir = os.path.join(work_dir, "backup") + os.makedirs(backup_dir) + + return mock.MagicMock( + work_dir=work_dir, backup_dir=backup_dir, + temp_checkpoint_dir=os.path.join(work_dir, "temp"), + in_progress_dir=os.path.join(backup_dir, "in_progress_dir")) def setup_test_files(): diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 944ae94ef..b92298a9a 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -125,11 +125,11 @@ def main(): # pylint: disable=too-many-branches sys.exit() if config.revoke: - client.revoke(config.acme_server, config) + client.revoke(config) sys.exit() if config.rollback > 0: - client.rollback(config.rollback, config) + client.rollback(config) sys.exit() if not config.eula: @@ -160,7 +160,7 @@ def main(): # pylint: disable=too-many-branches else: privkey = client.Client.Key(config.privkey[0], config.privkey[1]) - acme = client.Client(config.acme_server, privkey, auth, installer, config) + acme = client.Client(config, privkey, auth, installer) # Validate the key and csr client.validate_key_csr(privkey) From 4357c625c404fd51d47423c72fc337e86c9b990c Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 2 Feb 2015 19:17:03 +0000 Subject: [PATCH 08/19] Fix typo: rev_token_dir(s) --- letsencrypt/client/client_authenticator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/client/client_authenticator.py b/letsencrypt/client/client_authenticator.py index 2897a5132..5ac93c383 100644 --- a/letsencrypt/client/client_authenticator.py +++ b/letsencrypt/client/client_authenticator.py @@ -26,7 +26,7 @@ class ClientAuthenticator(object): """ self.rec_token = recovery_token.RecoveryToken( - config.acme_server, config.rev_token_dirs) + config.acme_server, config.rev_token_dir) def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" From 1d45b466a3fac24c6587406a131bad666e46ab3c Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 2 Feb 2015 21:11:59 +0000 Subject: [PATCH 09/19] IConfig.apache_server_root without trailing slash --- letsencrypt/client/apache/configurator.py | 13 +++---- letsencrypt/client/apache/dvsni.py | 7 ++-- letsencrypt/client/apache/parser.py | 25 ++++++++----- .../client/tests/apache/parser_test.py | 37 ++++++++++++++----- letsencrypt/client/tests/apache/util.py | 3 +- letsencrypt/scripts/main.py | 19 +++++----- 6 files changed, 62 insertions(+), 42 deletions(-) diff --git a/letsencrypt/client/apache/configurator.py b/letsencrypt/client/apache/configurator.py index 6b7e03f24..bafe94f54 100644 --- a/letsencrypt/client/apache/configurator.py +++ b/letsencrypt/client/apache/configurator.py @@ -317,7 +317,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ # Search sites-available, httpd.conf for possible virtual hosts paths = self.aug.match( - ("/files%ssites-available//*[label()=~regexp('%s')]" % + ("/files%s/sites-available//*[label()=~regexp('%s')]" % (self.parser.root, parser.case_i('VirtualHost')))) vhs = [] @@ -682,8 +682,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if ssl_vhost.names[0] < (255-23): redirect_filename = "le-redirect-%s.conf" % ssl_vhost.names[0] - redirect_filepath = ("%ssites-available/%s" % - (self.parser.root, redirect_filename)) + redirect_filepath = os.path.join( + self.parser.root, 'sites-available', redirect_filename) # Register the new file that will be created # Note: always register the creation before writing to ensure file will @@ -697,8 +697,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.aug.load() # Make a new vhost data structure and add it to the lists - new_fp = self.parser.root + "sites-available/" + redirect_filename - new_vhost = self._create_vhost(parser.get_aug_path(new_fp)) + new_vhost = self._create_vhost(parser.get_aug_path(redirect_filepath)) self.vhosts.append(new_vhost) # Finally create documentation for the change @@ -829,7 +828,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :rtype: bool """ - enabled_dir = os.path.join(self.parser.root, "sites-enabled/") + enabled_dir = os.path.join(self.parser.root, "sites-enabled") for entry in os.listdir(enabled_dir): if os.path.realpath(os.path.join(enabled_dir, entry)) == avail_fp: return True @@ -854,7 +853,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return True if "/sites-available/" in vhost.filep: - enabled_path = ("%ssites-enabled/%s" % + enabled_path = ("%s/sites-enabled/%s" % (self.parser.root, os.path.basename(vhost.filep))) self.reverter.register_file_creation(False, enabled_path) os.symlink(vhost.filep, enabled_path) diff --git a/letsencrypt/client/apache/dvsni.py b/letsencrypt/client/apache/dvsni.py index 79ea5f5c4..f9efdf559 100644 --- a/letsencrypt/client/apache/dvsni.py +++ b/letsencrypt/client/apache/dvsni.py @@ -167,6 +167,8 @@ class ApacheDvsni(object): """ ips = " ".join(str(i) for i in ip_addrs) + document_root = os.path.join( + self.configurator.config.config_dir, "dvsni_page/") return ("\n" "ServerName " + nonce + constants.DVSNI_DOMAIN_SUFFIX + "\n" "UseCanonicalName on\n" @@ -178,8 +180,7 @@ class ApacheDvsni(object): "SSLCertificateFile " + self.get_cert_file(nonce) + "\n" "SSLCertificateKeyFile " + dvsni_key_file + "\n" "\n" - "DocumentRoot " + - self.configurator.config.config_dir + "dvsni_page/\n" + "DocumentRoot " + document_root + "\n" "\n\n") def get_cert_file(self, nonce): @@ -191,4 +192,4 @@ class ApacheDvsni(object): :rtype: str """ - return self.configurator.config.work_dir + nonce + ".crt" + return os.path.join(self.configurator.config.work_dir, nonce + ".crt") diff --git a/letsencrypt/client/apache/parser.py b/letsencrypt/client/apache/parser.py index 8bc636dd1..0a5eff97c 100644 --- a/letsencrypt/client/apache/parser.py +++ b/letsencrypt/client/apache/parser.py @@ -6,18 +6,23 @@ from letsencrypt.client import errors class ApacheParser(object): - """Class handles the fine details of parsing the Apache Configuration.""" + """Class handles the fine details of parsing the Apache Configuration. + + :ivar str root: Normalized abosulte path to the server root + directory. Without trailing slash. + + """ def __init__(self, aug, root, ssl_options): # Find configuration root and make sure augeas can parse it. self.aug = aug - self.root = root + self.root = os.path.abspath(root) self.loc = self._set_locations(ssl_options) self._parse_file(self.loc["root"]) # Must also attempt to parse sites-available or equivalent # Sites-available is not included naturally in configuration - self._parse_file(os.path.join(self.root, "sites-available/*")) + self._parse_file(os.path.join(self.root, "sites-available") + "/*") # This problem has been fixed in Augeas 1.0 self.standardize_excl() @@ -190,7 +195,7 @@ class ApacheParser(object): arg = cur_dir + arg # conf/ is a special variable for ServerRoot in Apache elif arg.startswith("conf/"): - arg = self.root + arg[5:] + arg = self.root + arg[4:] # TODO: Test if Apache allows ../ or ~/ for Includes # Attempts to add a transform to the file if one does not already exist @@ -301,12 +306,12 @@ class ApacheParser(object): excl = ["*.augnew", "*.augsave", "*.dpkg-dist", "*.dpkg-bak", "*.dpkg-new", "*.dpkg-old", "*.rpmsave", "*.rpmnew", "*~", - self.root + "*.augsave", - self.root + "*~", - self.root + "*/*augsave", - self.root + "*/*~", - self.root + "*/*/*.augsave", - self.root + "*/*/*~"] + self.root + "/*.augsave", + self.root + "/*~", + self.root + "/*/*augsave", + self.root + "/*/*~", + self.root + "/*/*/*.augsave", + self.root + "/*/*/*~"] for i in range(len(excl)): self.aug.set("/augeas/load/Httpd/excl[%d]" % (i+1), excl[i]) diff --git a/letsencrypt/client/tests/apache/parser_test.py b/letsencrypt/client/tests/apache/parser_test.py index 453952a19..fe9e96ed5 100644 --- a/letsencrypt/client/tests/apache/parser_test.py +++ b/letsencrypt/client/tests/apache/parser_test.py @@ -10,7 +10,6 @@ import zope.component from letsencrypt.client import display from letsencrypt.client import errors -from letsencrypt.client.apache import parser from letsencrypt.client.tests.apache import util @@ -23,15 +22,32 @@ class ApacheParserTest(util.ApacheTest): zope.component.provideUtility(display.FileDisplay(sys.stdout)) - self.parser = parser.ApacheParser( - augeas.Augeas(flags=augeas.Augeas.NONE), - self.config_path, self.ssl_options) + from letsencrypt.client.apache.parser import ApacheParser + self.aug = augeas.Augeas(flags=augeas.Augeas.NONE) + self.parser = ApacheParser(self.aug, self.config_path, self.ssl_options) def tearDown(self): shutil.rmtree(self.temp_dir) shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) + def test_root_normalized(self): + from letsencrypt.client.apache.parser import ApacheParser + path = os.path.join(self.temp_dir, "debian_apache_2_4/////" + "two_vhost_80/../two_vhost_80/apache2") + parser = ApacheParser(self.aug, path, None) + self.assertEqual(parser.root, self.config_path) + + def test_root_absolute(self): + from letsencrypt.client.apache.parser import ApacheParser + parser = ApacheParser(self.aug, os.path.relpath(self.config_path), None) + self.assertEqual(parser.root, self.config_path) + + def test_root_no_trailing_slash(self): + from letsencrypt.client.apache.parser import ApacheParser + parser = ApacheParser(self.aug, self.config_path + os.path.sep, None) + self.assertEqual(parser.root, self.config_path) + def test_parse_file(self): """Test parse_file. @@ -51,10 +67,10 @@ class ApacheParserTest(util.ApacheTest): self.assertTrue(matches) def test_find_dir(self): - test = self.parser.find_dir(parser.case_i("Listen"), "443") + from letsencrypt.client.apache.parser import case_i + test = self.parser.find_dir(case_i("Listen"), "443") # This will only look in enabled hosts - test2 = self.parser.find_dir( - parser.case_i("documentroot")) + test2 = self.parser.find_dir(case_i("documentroot")) self.assertEqual(len(test), 2) self.assertEqual(len(test2), 3) @@ -76,8 +92,9 @@ class ApacheParserTest(util.ApacheTest): Path must be valid before attempting to add to augeas """ + from letsencrypt.client.apache.parser import get_aug_path self.parser.add_dir_to_ifmodssl( - parser.get_aug_path(self.parser.loc["default"]), + get_aug_path(self.parser.loc["default"]), "FakeDirective", "123") matches = self.parser.find_dir("FakeDirective", "123") @@ -86,8 +103,8 @@ class ApacheParserTest(util.ApacheTest): self.assertTrue("IfModule" in matches[0]) def test_get_aug_path(self): - self.assertEqual( - "/files/etc/apache", parser.get_aug_path("/etc/apache")) + from letsencrypt.client.apache.parser import get_aug_path + self.assertEqual("/files/etc/apache", get_aug_path("/etc/apache")) def test_set_locations(self): with mock.patch("letsencrypt.client.apache.parser." diff --git a/letsencrypt/client/tests/apache/util.py b/letsencrypt/client/tests/apache/util.py index df981aede..78566e1e4 100644 --- a/letsencrypt/client/tests/apache/util.py +++ b/letsencrypt/client/tests/apache/util.py @@ -22,9 +22,8 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods self.ssl_options = setup_apache_ssl_options(self.config_dir) - # Final slash is currently important self.config_path = os.path.join( - self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2/") + self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2") self.rsa256_file = pkg_resources.resource_filename( "letsencrypt.client.tests", 'testdata/rsa256_key.pem') diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index b92298a9a..a6a12f725 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -54,26 +54,25 @@ def create_parser(): help="Use the text output instead of the curses UI.") add("--test", action="store_true", help="Run in test mode.") - # TODO: trailing slashes might be important! check and remove - add("--config-dir", default="/etc/letsencrypt/", + add("--config-dir", default="/etc/letsencrypt", help="Configuration directory.") - add("--work-dir", default="/var/lib/letsencrypt/", + add("--work-dir", default="/var/lib/letsencrypt", help="Working directory.") - add("--backup-dir", default="/var/lib/letsencrypt/backups/", + add("--backup-dir", default="/var/lib/letsencrypt/backups", help="Configuration backups directory.") add("--temp-checkpoint-dir", - default="/var/lib/letsencrypt/temp_checkpoint/", + default="/var/lib/letsencrypt/temp_checkpoint", help="Temporary checkpoint directory.") add("--in-progress-dir", - default="/var/lib/letsencrypt/backups/IN_PROGRESS/", + default="/var/lib/letsencrypt/backups/IN_PROGRESS", help="Directory used before a permanent checkpoint is finalized") - add("--cert-key-backup", default="/var/lib/letsencrypt/keys-certs/", + add("--cert-key-backup", default="/var/lib/letsencrypt/keys-certs", help="Directory where all certificates and keys are stored. " "Used for easy revocation.") - add("--rev-tokens-dir", default="/var/lib/letsencrypt/revocation_tokens/", + add("--rev-tokens-dir", default="/var/lib/letsencrypt/revocation_tokens", help="Directory where all revocation tokens are saved.") - add("--key-dir", default="/etc/letsencrypt/keys/", help="Keys storage.") - add("--cert-dir", default="/etc/letsencrypt/certs/", + add("--key-dir", default="/etc/letsencrypt/keys", help="Keys storage.") + add("--cert-dir", default="/etc/letsencrypt/certs", help="Certificates storage.") add("--le-vhost-ext", default="-le-ssl.conf", From 9fb56f31d3674f69f49155b334dc2ec8e3390e73 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 3 Feb 2015 11:22:04 +0000 Subject: [PATCH 10/19] NamespaceConfig --- letsencrypt/client/configuration.py | 35 +++++++++++++++++++++++++++++ letsencrypt/client/constants.py | 16 +++++++++++++ letsencrypt/client/interfaces.py | 2 +- letsencrypt/scripts/main.py | 20 +++++------------ 4 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 letsencrypt/client/configuration.py diff --git a/letsencrypt/client/configuration.py b/letsencrypt/client/configuration.py new file mode 100644 index 000000000..a39bf4732 --- /dev/null +++ b/letsencrypt/client/configuration.py @@ -0,0 +1,35 @@ +"""Let's Encrypt user-supplied configuration.""" +import zope.interface + +from letsencrypt.client import interfaces + + +class NamespaceConfig(object): + """Configuration wrapper around `argparse.Namespace`.""" + zope.interface.implements(interfaces.IConfig) + + def __init__(self, namespace): + self.namespace = namespace + + def __getattr__(self, name): + return getattr(self.namespace, name) + + @property + def temp_checkpoint_dir(self): + return os.path.join( + self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR_NAME) + + @property + def in_progress_dir(self): + return os.path.join( + self.namespace.work_dir, constants.IN_PROGRESS_DIR_NAME) + + @property + def cert_key_backup(self): + return os.path.join( + self.namespace.work_dir, constants.CERT_KEY_BACKUP_DIR_NAME) + + @property + def rev_tokens_dir(self): + return os.path.join( + self.namespace.work_dir, constants.REV_TOKENS_DIR_NAME) diff --git a/letsencrypt/client/constants.py b/letsencrypt/client/constants.py index d07737ac7..3414bce74 100644 --- a/letsencrypt/client/constants.py +++ b/letsencrypt/client/constants.py @@ -47,3 +47,19 @@ APACHE_REWRITE_HTTPS_ARGS = [ DVSNI_DOMAIN_SUFFIX = ".acme.invalid" """Suffix appended to domains in DVSNI validation.""" + + +TEMP_CHECKPOINT_DIR_NAME = "temp_checkpoint" +"""Temporary checkpoint directory (relative to IConfig.work_dir).""" + +IN_PROGRESS_DIR_NAME = "IN_PROGRESS" +"""Directory used before a permanent checkpoint is finalized (relative to +IConfig.work_dir).""" + +CERT_KEY_BACKUP_DIR_NAME = "keys-certs" +"""Directory where all certificates and keys are stored (relative to +IConfig.work_dir. Used for easy revocation.""" + +REV_TOKENS_DIR_NAME = "revocation_tokens" +"""Directory where all revocation tokens are saved (relative to +IConfig.work_dir).""" diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index c4f28afdb..f9c614614 100644 --- a/letsencrypt/client/interfaces.py +++ b/letsencrypt/client/interfaces.py @@ -60,7 +60,7 @@ class IChallenge(zope.interface.Interface): class IConfig(zope.interface.Interface): - """Marker interface for Let's Encrypt config.""" + """Let's Encrypt uesr-supplied configuration.""" class IInstaller(zope.interface.Interface): diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index a6a12f725..62e007128 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -11,10 +11,11 @@ import sys import confargparse import zope.component -import zope.interface import letsencrypt +from letsencrypt.client import constants +from letsencrypt.client import configuration from letsencrypt.client import client from letsencrypt.client import display from letsencrypt.client import errors @@ -60,17 +61,6 @@ def create_parser(): help="Working directory.") add("--backup-dir", default="/var/lib/letsencrypt/backups", help="Configuration backups directory.") - add("--temp-checkpoint-dir", - default="/var/lib/letsencrypt/temp_checkpoint", - help="Temporary checkpoint directory.") - add("--in-progress-dir", - default="/var/lib/letsencrypt/backups/IN_PROGRESS", - help="Directory used before a permanent checkpoint is finalized") - add("--cert-key-backup", default="/var/lib/letsencrypt/keys-certs", - help="Directory where all certificates and keys are stored. " - "Used for easy revocation.") - add("--rev-tokens-dir", default="/var/lib/letsencrypt/revocation_tokens", - help="Directory where all revocation tokens are saved.") add("--key-dir", default="/etc/letsencrypt/keys", help="Keys storage.") add("--cert-dir", default="/etc/letsencrypt/certs", help="Certificates storage.") @@ -96,12 +86,12 @@ def create_parser(): return parser + def main(): # pylint: disable=too-many-branches """Command line argument parsing and main script execution.""" - # note: arg parser internally handles --help (and exits afterwards) - config = create_parser().parse_args() - zope.interface.directlyProvides(config, interfaces.IConfig) + args = create_parser().parse_args() + config = configuration.NamespaceConfig(args) # note: check is done after arg parsing as --help should work w/o root also. if not os.geteuid() == 0: From 43e207f9d06740888b56def858d8f1ffba602983 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 3 Feb 2015 12:10:51 +0000 Subject: [PATCH 11/19] API docs: Remove CONFIG, add configuration. --- docs/api/client/CONFIG.rst | 5 ----- docs/api/client/configuration.rst | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 docs/api/client/CONFIG.rst create mode 100644 docs/api/client/configuration.rst diff --git a/docs/api/client/CONFIG.rst b/docs/api/client/CONFIG.rst deleted file mode 100644 index 9ed190768..000000000 --- a/docs/api/client/CONFIG.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt.client.CONFIG` --------------------------------- - -.. automodule:: letsencrypt.client.CONFIG - :members: diff --git a/docs/api/client/configuration.rst b/docs/api/client/configuration.rst new file mode 100644 index 000000000..0bec61480 --- /dev/null +++ b/docs/api/client/configuration.rst @@ -0,0 +1,5 @@ +:mod:`letsencrypt.client.configuration` +--------------------------------------- + +.. automodule:: letsencrypt.client.configuration + :members: From 207bd6c31c7ce56e6a8f02a2b99b7d772567af9b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 3 Feb 2015 12:13:57 +0000 Subject: [PATCH 12/19] IConfig attributes. config/args separation --- letsencrypt/client/client.py | 14 ++++-- letsencrypt/client/configuration.py | 10 ++-- letsencrypt/client/interfaces.py | 39 ++++++++++++++- letsencrypt/client/tests/client_test.py | 2 +- letsencrypt/scripts/main.py | 64 ++++++++++++------------- 5 files changed, 84 insertions(+), 45 deletions(-) diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 6cc1ad81c..7f48e80e0 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -421,7 +421,7 @@ def determine_installer(config): logging.info("Unable to find a way to install the certificate.") -def rollback(config): +def rollback(checkpoints, config): """Revert configuration the specified number of checkpoints. .. note:: If another installer uses something other than the reverter class @@ -436,6 +436,8 @@ def rollback(config): of future installers. Perhaps the interface should define errors that are thrown for the various functions. + :param int checkpoints: Number of checkpoints to revert. + :param config: Configuration. :type config: :class:`letsencrypt.client.interfaces.IConfig` @@ -444,20 +446,22 @@ def rollback(config): try: installer = determine_installer(config) except errors.LetsEncryptMisconfigurationError: - _misconfigured_rollback(config) + _misconfigured_rollback(checkpoints, config) return # No Errors occurred during init... proceed normally # If installer is None... couldn't find an installer... there shouldn't be # anything to rollback if installer is not None: - installer.rollback_checkpoints(config.rollback) + installer.rollback_checkpoints(checkpoints) installer.restart() -def _misconfigured_rollback(config): +def _misconfigured_rollback(checkpoints, config): """Handles the case where the Installer is misconfigured. + :param int checkpoints: Number of checkpoints to revert. + :param config: Configuration. :type config: :class:`letsencrypt.client.interfaces.IConfig` @@ -477,7 +481,7 @@ def _misconfigured_rollback(config): # Also... not sure how future installers will handle recovery. rev = reverter.Reverter(config) rev.recovery_routine() - rev.rollback_checkpoints(config.rollback) + rev.rollback_checkpoints(checkpoints) # We should try to restart the server try: diff --git a/letsencrypt/client/configuration.py b/letsencrypt/client/configuration.py index a39bf4732..5b7568c1f 100644 --- a/letsencrypt/client/configuration.py +++ b/letsencrypt/client/configuration.py @@ -1,6 +1,8 @@ """Let's Encrypt user-supplied configuration.""" +import os import zope.interface +from letsencrypt.client import constants from letsencrypt.client import interfaces @@ -15,21 +17,21 @@ class NamespaceConfig(object): return getattr(self.namespace, name) @property - def temp_checkpoint_dir(self): + def temp_checkpoint_dir(self): # pylint: disable=missing-docstring return os.path.join( self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR_NAME) @property - def in_progress_dir(self): + def in_progress_dir(self): # pylint: disable=missing-docstring return os.path.join( self.namespace.work_dir, constants.IN_PROGRESS_DIR_NAME) @property - def cert_key_backup(self): + def cert_key_backup(self): # pylint: disable=missing-docstring return os.path.join( self.namespace.work_dir, constants.CERT_KEY_BACKUP_DIR_NAME) @property - def rev_tokens_dir(self): + def rev_tokens_dir(self): # pylint: disable=missing-docstring return os.path.join( self.namespace.work_dir, constants.REV_TOKENS_DIR_NAME) diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index f9c614614..9516ba95d 100644 --- a/letsencrypt/client/interfaces.py +++ b/letsencrypt/client/interfaces.py @@ -60,7 +60,44 @@ class IChallenge(zope.interface.Interface): class IConfig(zope.interface.Interface): - """Let's Encrypt uesr-supplied configuration.""" + """Let's Encrypt user-supplied configuration.""" + + acme_server = zope.interface.Attribute( + "CA hostname (and optionally :port). The server certificate must " + "be trusted in order to avoid further modifications to the client.") + rsa_key_size = zope.interface.Attribute("Size of the RSA key.") + + config_dir = zope.interface.Attribute("Configuration directory.") + work_dir = zope.interface.Attribute("Working directory.") + backup_dir = zope.interface.Attribute("Configuration backups directory.") + temp_checkpoint_dir = zope.interface.Attribute( + "Temporary checkpoint directory.") + in_progress_dir = zope.interface.Attribute( + "Directory used before a permanent checkpoint is finalized.") + cert_key_backup = zope.interface.Attribute( + "Directory where all certificates and keys are stored. " + "Used for easy revocation.") + rev_tokens_dir = zope.interface.Attribute( + "Directory where all revocation tokens are saved.") + key_dir = zope.interface.Attribute("Keys storage.") + cert_dir = zope.interface.Attribute("Certificates storage.") + + le_vhost_ext = zope.interface.Attribute( + "SSL vhost configuration extension.") + cert_path = zope.interface.Attribute("Let's Encrypt certificate file.") + chain_path = zope.interface.Attribute("Let's Encrypt chain file.") + + apache_server_root = zope.interface.Attribute( + "Apache server root directory.") + apache_ctl = zope.interface.Attribute( + "Path to the 'apache2ctl' binary, used for 'configtest' and " + "retrieving Apache2 version number.") + apache_enmod = zope.interface.Attribute( + "Path to the Apache 'a2enmod' binary.") + apache_init_script = zope.interface.Attribute( + "Path to the Apache init script (used for server reload/restart).") + apache_mod_ssl_conf = zope.interface.Attribute( + "Contains standard Apache SSL directives.") class IInstaller(zope.interface.Interface): diff --git a/letsencrypt/client/tests/client_test.py b/letsencrypt/client/tests/client_test.py index 6b9235e11..df07d1fa9 100644 --- a/letsencrypt/client/tests/client_test.py +++ b/letsencrypt/client/tests/client_test.py @@ -14,7 +14,7 @@ class RollbackTest(unittest.TestCase): @classmethod def _call(cls, checkpoints): from letsencrypt.client.client import rollback - rollback(mock.MagicMock(rollback=checkpoints)) + rollback(checkpoints, mock.MagicMock()) @mock.patch("letsencrypt.client.client.determine_installer") def test_no_problems(self, mock_det): diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 62e007128..7eeb8ebe7 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -14,7 +14,6 @@ import zope.component import letsencrypt -from letsencrypt.client import constants from letsencrypt.client import configuration from letsencrypt.client import client from letsencrypt.client import display @@ -28,17 +27,16 @@ def create_parser(): description="letsencrypt client %s" % letsencrypt.__version__) add = parser.add_argument + config_help = lambda name: interfaces.IConfig[name].__doc__ add("-d", "--domains", metavar="DOMAIN", nargs="+") add("-s", "--acme-server", "--server", default="letsencrypt-demo.org:443", - help="CA hostname (and optionally :port). The server certificate must " - "be trusted in order to avoid further modifications to the " - "client.") + help=config_help("acme_server")) add("-p", "--privkey", type=read_file, help="Path to the private key file for certificate generation.") - add("-B", "--rsa-key-size", type=int, default=2048, - metavar="N", help="RSA key shall be sized N bits.") + add("-B", "--rsa-key-size", type=int, default=2048, metavar="N", + help=config_help("rsa_key_size")) add("-k", "--revoke", action="store_true", help="Revoke a certificate.") add("-b", "--rollback", type=int, default=0, metavar="N", @@ -56,33 +54,31 @@ def create_parser(): add("--test", action="store_true", help="Run in test mode.") add("--config-dir", default="/etc/letsencrypt", - help="Configuration directory.") + help=config_help("config_dir")) add("--work-dir", default="/var/lib/letsencrypt", - help="Working directory.") + help=config_help("work_dir")) add("--backup-dir", default="/var/lib/letsencrypt/backups", - help="Configuration backups directory.") - add("--key-dir", default="/etc/letsencrypt/keys", help="Keys storage.") + help=config_help("backup_dir")) + add("--key-dir", default="/etc/letsencrypt/keys", + help=config_help("key_dir")) add("--cert-dir", default="/etc/letsencrypt/certs", - help="Certificates storage.") + help=config_help("cert_dir")) add("--le-vhost-ext", default="-le-ssl.conf", - help="SSL vhost configuration extension.") + help=config_help("le_vhost_ext")) add("--cert-path", default="/etc/letsencrypt/certs/cert-letsencrypt.pem", - help="Let's Encrypt certificate file.") + help=config_help("cert_path")) add("--chain-path", default="/etc/letsencrypt/certs/chain-letsencrypt.pem", - help="Let's Encrypt chain file.") + help=config_help("chain_path")) - add("--apache-ctl", default="apache2ctl", - help="Path to the 'apache2ctl' binary, used for 'configtest' and " - "retrieving Apache2 version number.") - add("--apache-enmod", default="a2enmod", - help="Path to the Apache 'a2enmod' binary.") - add("--apache-init-script", default="/etc/init.d/apache2", - help="Path to the Apache init script (used for server reload/restart).") add("--apache-server-root", default="/etc/apache2", - help="Apache server root directory.") + help=config_help("apache_server_root")) add("--apache-mod-ssl-conf", default="/etc/letsencrypt/options-ssl.conf", - help="Contains standard Apache SSL directives.") + help=config_help("apache_mod_ssl_conf")) + add("--apache-ctl", default="apache2ctl", help=config_help("apache_ctl")) + add("--apache-enmod", default="a2enmod", help=config_help("apache_enmod")) + add("--apache-init-script", default="/etc/init.d/apache2", + help=config_help("apache_init_script")) return parser @@ -102,26 +98,26 @@ def main(): # pylint: disable=too-many-branches # Set up logging logger = logging.getLogger() logger.setLevel(logging.INFO) - if config.use_curses: + if args.use_curses: logger.addHandler(log.DialogHandler()) displayer = display.NcursesDisplay() else: displayer = display.FileDisplay(sys.stdout) zope.component.provideUtility(displayer) - if config.view_config_changes: + if args.view_config_changes: client.view_config_changes(config) sys.exit() - if config.revoke: + if args.revoke: client.revoke(config) sys.exit() - if config.rollback > 0: - client.rollback(config) + if args.rollback > 0: + client.rollback(args.rollback, config) sys.exit() - if not config.eula: + if not args.eula: display_eula() # Make sure we actually get an installer that is functioning properly @@ -140,14 +136,14 @@ def main(): # pylint: disable=too-many-branches else: auth = client.determine_authenticator(config) - if config.domains is None: + if args.domains is None: domains = choose_names(installer) # Prepare for init of Client - if config.privkey is None: - privkey = client.init_key(config.rsa_key_size, config.key_dir) + if args.privkey is None: + privkey = client.init_key(args.rsa_key_size, config.key_dir) else: - privkey = client.Client.Key(config.privkey[0], config.privkey[1]) + privkey = client.Client.Key(args.privkey[0], args.privkey[1]) acme = client.Client(config, privkey, auth, installer) @@ -163,7 +159,7 @@ def main(): # pylint: disable=too-many-branches if installer is not None and cert_file is not None: acme.deploy_certificate(domains, privkey, cert_file, chain_file) if installer is not None: - acme.enhance_config(domains, config.redirect) + acme.enhance_config(domains, args.redirect) def display_eula(): From e9512e5a46b8743fd35c23dfa9eb374930d058a8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 4 Feb 2015 16:38:13 +0000 Subject: [PATCH 13/19] Add tests and docs for IConfig/NamespaceConfig --- letsencrypt/client/configuration.py | 29 ++++++++++++---- letsencrypt/client/constants.py | 8 ++--- letsencrypt/client/interfaces.py | 7 +++- .../client/tests/configuration_test.py | 33 +++++++++++++++++++ 4 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 letsencrypt/client/tests/configuration_test.py diff --git a/letsencrypt/client/configuration.py b/letsencrypt/client/configuration.py index 5b7568c1f..048746b12 100644 --- a/letsencrypt/client/configuration.py +++ b/letsencrypt/client/configuration.py @@ -7,7 +7,24 @@ from letsencrypt.client import interfaces class NamespaceConfig(object): - """Configuration wrapper around `argparse.Namespace`.""" + """Configuration wrapper around :class:`argparse.Namespace`. + + For more documentation, including available attributes, please see + :class:`letsencrypt.client.interfaces.IConfig`. However, note that + the following attributes are dynamically resolved using + :attr:`~letsencrypt.client.interfaces.IConfig.work_dir` and relative + paths defined in :py:mod:`letsencrypt.client.constants`: + + - ``temp_checkpoint_dir`` + - ``in_progress_dir`` + - ``cert_key_backup`` + - ``rev_tokens_dir`` + + :ivar namespace: Namespace typically produced by + :meth:`argparse.ArgumentParser.parse_args`. + :type namespace: :class:`argparse.Namespace` + + """ zope.interface.implements(interfaces.IConfig) def __init__(self, namespace): @@ -19,19 +36,17 @@ class NamespaceConfig(object): @property def temp_checkpoint_dir(self): # pylint: disable=missing-docstring return os.path.join( - self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR_NAME) + self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR) @property def in_progress_dir(self): # pylint: disable=missing-docstring - return os.path.join( - self.namespace.work_dir, constants.IN_PROGRESS_DIR_NAME) + return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR) @property def cert_key_backup(self): # pylint: disable=missing-docstring return os.path.join( - self.namespace.work_dir, constants.CERT_KEY_BACKUP_DIR_NAME) + self.namespace.work_dir, constants.CERT_KEY_BACKUP_DIR) @property def rev_tokens_dir(self): # pylint: disable=missing-docstring - return os.path.join( - self.namespace.work_dir, constants.REV_TOKENS_DIR_NAME) + return os.path.join(self.namespace.work_dir, constants.REV_TOKENS_DIR) diff --git a/letsencrypt/client/constants.py b/letsencrypt/client/constants.py index 3414bce74..e1a8ebaed 100644 --- a/letsencrypt/client/constants.py +++ b/letsencrypt/client/constants.py @@ -49,17 +49,17 @@ DVSNI_DOMAIN_SUFFIX = ".acme.invalid" """Suffix appended to domains in DVSNI validation.""" -TEMP_CHECKPOINT_DIR_NAME = "temp_checkpoint" +TEMP_CHECKPOINT_DIR = "temp_checkpoint" """Temporary checkpoint directory (relative to IConfig.work_dir).""" -IN_PROGRESS_DIR_NAME = "IN_PROGRESS" +IN_PROGRESS_DIR = "IN_PROGRESS" """Directory used before a permanent checkpoint is finalized (relative to IConfig.work_dir).""" -CERT_KEY_BACKUP_DIR_NAME = "keys-certs" +CERT_KEY_BACKUP_DIR = "keys-certs" """Directory where all certificates and keys are stored (relative to IConfig.work_dir. Used for easy revocation.""" -REV_TOKENS_DIR_NAME = "revocation_tokens" +REV_TOKENS_DIR = "revocation_tokens" """Directory where all revocation tokens are saved (relative to IConfig.work_dir).""" diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index 9516ba95d..4f7dcda45 100644 --- a/letsencrypt/client/interfaces.py +++ b/letsencrypt/client/interfaces.py @@ -60,7 +60,12 @@ class IChallenge(zope.interface.Interface): class IConfig(zope.interface.Interface): - """Let's Encrypt user-supplied configuration.""" + """Let's Encrypt user-supplied configuration. + + .. warning:: The values stored in the configuration have not been + filtered, stripped or sanitized in any way! + + """ acme_server = zope.interface.Attribute( "CA hostname (and optionally :port). The server certificate must " diff --git a/letsencrypt/client/tests/configuration_test.py b/letsencrypt/client/tests/configuration_test.py new file mode 100644 index 000000000..8e4a9def1 --- /dev/null +++ b/letsencrypt/client/tests/configuration_test.py @@ -0,0 +1,33 @@ +"""Tests for letsencrypt.client.configuration.""" +import functools +import unittest + +import mock + + +class NamespaceConfigTest(unittest.TestCase): + """Tests for letsencrypt.client.configuration.NamespaceConfig.""" + + def setUp(self): + from letsencrypt.client.configuration import NamespaceConfig + namespace = mock.MagicMock(work_dir='/tmp/foo', foo='bar') + self.config = NamespaceConfig(namespace) + + def test_proxy_getattr(self): + self.assertEqual(self.config.foo, 'bar') + self.assertEqual(self.config.work_dir, '/tmp/foo') + + @mock.patch('letsencrypt.client.configuration.constants') + def test_dynamic_dirs(self, constants): + constants.TEMP_CHECKPOINT_DIR = 't' + constants.IN_PROGRESS_DIR = '../p' + constants.CERT_KEY_BACKUP_DIR = 'c/' + constants.REV_TOKENS_DIR = '/r' + self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t') + self.assertEqual(self.config.in_progress_dir, '/tmp/foo/../p') + self.assertEqual(self.config.cert_key_backup, '/tmp/foo/c/') + self.assertEqual(self.config.rev_tokens_dir, '/r') + + +if __name__ == '__main__': + unittest.main() From f910b7ee6b2ac03da4428ce4c79ceb52c98bc83d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 4 Feb 2015 16:40:57 +0000 Subject: [PATCH 14/19] Remove unused import --- letsencrypt/client/tests/configuration_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/letsencrypt/client/tests/configuration_test.py b/letsencrypt/client/tests/configuration_test.py index 8e4a9def1..efc645ad4 100644 --- a/letsencrypt/client/tests/configuration_test.py +++ b/letsencrypt/client/tests/configuration_test.py @@ -1,5 +1,4 @@ """Tests for letsencrypt.client.configuration.""" -import functools import unittest import mock From a0c184f2921d1cc33a1ac9fe4770407283c1e111 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Feb 2015 22:36:44 +0000 Subject: [PATCH 15/19] Update docs references for constants.ENHANCEMENTS --- letsencrypt/client/interfaces.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index 4f7dcda45..5f6539940 100644 --- a/letsencrypt/client/interfaces.py +++ b/letsencrypt/client/interfaces.py @@ -129,10 +129,10 @@ class IInstaller(zope.interface.Interface): :param str domain: domain for which to provide enhancement :param str enhancement: An enhancement as defined in - :const:`~letsencrypt.client.CONFIG.ENHANCEMENTS` + :const:`~letsencrypt.client.constants.ENHANCEMENTS` :param options: Flexible options parameter for enhancement. Check documentation of - :const:`~letsencrypt.client.CONFIG.ENHANCEMENTS` + :const:`~letsencrypt.client.constants.ENHANCEMENTS` for expected options for each enhancement. """ @@ -141,7 +141,7 @@ class IInstaller(zope.interface.Interface): """Returns a list of supported enhancements. :returns: supported enhancements which should be a subset of - :const:`~letsencrypt.client.CONFIG.ENHANCEMENTS` + :const:`~letsencrypt.client.constants.ENHANCEMENTS` :rtype: :class:`list` of :class:`str` """ From f476a49387c3dc4682ad703d78cebf702129abb3 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 9 Feb 2015 14:39:49 -0800 Subject: [PATCH 16/19] Fix #220 --- letsencrypt/client/client_authenticator.py | 2 +- letsencrypt/client/configuration.py | 6 +++--- letsencrypt/client/constants.py | 4 ++-- letsencrypt/client/interfaces.py | 6 +++--- letsencrypt/client/tests/configuration_test.py | 4 ++-- letsencrypt/scripts/main.py | 9 ++++----- 6 files changed, 15 insertions(+), 16 deletions(-) diff --git a/letsencrypt/client/client_authenticator.py b/letsencrypt/client/client_authenticator.py index 5ac93c383..7229239dc 100644 --- a/letsencrypt/client/client_authenticator.py +++ b/letsencrypt/client/client_authenticator.py @@ -26,7 +26,7 @@ class ClientAuthenticator(object): """ self.rec_token = recovery_token.RecoveryToken( - config.acme_server, config.rev_token_dir) + config.server, config.rec_token_dir) def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" diff --git a/letsencrypt/client/configuration.py b/letsencrypt/client/configuration.py index 048746b12..1bdbe2059 100644 --- a/letsencrypt/client/configuration.py +++ b/letsencrypt/client/configuration.py @@ -18,7 +18,7 @@ class NamespaceConfig(object): - ``temp_checkpoint_dir`` - ``in_progress_dir`` - ``cert_key_backup`` - - ``rev_tokens_dir`` + - ``rec_token_dir`` :ivar namespace: Namespace typically produced by :meth:`argparse.ArgumentParser.parse_args`. @@ -48,5 +48,5 @@ class NamespaceConfig(object): self.namespace.work_dir, constants.CERT_KEY_BACKUP_DIR) @property - def rev_tokens_dir(self): # pylint: disable=missing-docstring - return os.path.join(self.namespace.work_dir, constants.REV_TOKENS_DIR) + def rec_token_dir(self): # pylint: disable=missing-docstring + return os.path.join(self.namespace.work_dir, constants.REC_TOKEN_DIR) diff --git a/letsencrypt/client/constants.py b/letsencrypt/client/constants.py index e1a8ebaed..3652face7 100644 --- a/letsencrypt/client/constants.py +++ b/letsencrypt/client/constants.py @@ -60,6 +60,6 @@ CERT_KEY_BACKUP_DIR = "keys-certs" """Directory where all certificates and keys are stored (relative to IConfig.work_dir. Used for easy revocation.""" -REV_TOKENS_DIR = "revocation_tokens" -"""Directory where all revocation tokens are saved (relative to +REC_TOKEN_DIR = "recovery_tokens" +"""Directory where all recovery tokens are saved (relative to IConfig.work_dir).""" diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index 5f6539940..8ae995d4f 100644 --- a/letsencrypt/client/interfaces.py +++ b/letsencrypt/client/interfaces.py @@ -67,7 +67,7 @@ class IConfig(zope.interface.Interface): """ - acme_server = zope.interface.Attribute( + server = zope.interface.Attribute( "CA hostname (and optionally :port). The server certificate must " "be trusted in order to avoid further modifications to the client.") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") @@ -82,8 +82,8 @@ class IConfig(zope.interface.Interface): cert_key_backup = zope.interface.Attribute( "Directory where all certificates and keys are stored. " "Used for easy revocation.") - rev_tokens_dir = zope.interface.Attribute( - "Directory where all revocation tokens are saved.") + rec_token_dir = zope.interface.Attribute( + "Directory where all recovery tokens are saved.") key_dir = zope.interface.Attribute("Keys storage.") cert_dir = zope.interface.Attribute("Certificates storage.") diff --git a/letsencrypt/client/tests/configuration_test.py b/letsencrypt/client/tests/configuration_test.py index efc645ad4..a07953396 100644 --- a/letsencrypt/client/tests/configuration_test.py +++ b/letsencrypt/client/tests/configuration_test.py @@ -21,11 +21,11 @@ class NamespaceConfigTest(unittest.TestCase): constants.TEMP_CHECKPOINT_DIR = 't' constants.IN_PROGRESS_DIR = '../p' constants.CERT_KEY_BACKUP_DIR = 'c/' - constants.REV_TOKENS_DIR = '/r' + constants.REC_TOKEN_DIR = '/r' self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t') self.assertEqual(self.config.in_progress_dir, '/tmp/foo/../p') self.assertEqual(self.config.cert_key_backup, '/tmp/foo/c/') - self.assertEqual(self.config.rev_tokens_dir, '/r') + self.assertEqual(self.config.rec_token_dir, '/r') if __name__ == '__main__': diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 7eeb8ebe7..bb080ce4f 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -30,8 +30,8 @@ def create_parser(): config_help = lambda name: interfaces.IConfig[name].__doc__ add("-d", "--domains", metavar="DOMAIN", nargs="+") - add("-s", "--acme-server", "--server", default="letsencrypt-demo.org:443", - help=config_help("acme_server")) + add("-s", "--server", default="letsencrypt-demo.org:443", + help=config_help("server")) add("-p", "--privkey", type=read_file, help="Path to the private key file for certificate generation.") @@ -43,7 +43,7 @@ def create_parser(): help="Revert configuration N number of checkpoints.") add("-v", "--view-config-changes", action="store_true", help="View checkpoints and associated configuration changes.") - add("-r", "--redirect", action="store_true", + add("-r", "--redirect", type=bool, default=None, help="Automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost.") @@ -51,8 +51,7 @@ def create_parser(): help="Skip the end user license agreement screen.") add("-t", "--text", dest="use_curses", action="store_false", help="Use the text output instead of the curses UI.") - add("--test", action="store_true", help="Run in test mode.") - + add("--config-dir", default="/etc/letsencrypt", help=config_help("config_dir")) add("--work-dir", default="/var/lib/letsencrypt", From e04ab25642bb8835e304ec46a2b5fcc05f1736f8 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 9 Feb 2015 14:53:51 -0800 Subject: [PATCH 17/19] remove whitespace --- letsencrypt/scripts/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index bb080ce4f..784f8d691 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -51,7 +51,7 @@ def create_parser(): help="Skip the end user license agreement screen.") add("-t", "--text", dest="use_curses", action="store_false", help="Use the text output instead of the curses UI.") - + add("--config-dir", default="/etc/letsencrypt", help=config_help("config_dir")) add("--work-dir", default="/var/lib/letsencrypt", From 9beded8cfb3f854862b34e77552413173543771f Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 9 Feb 2015 16:04:46 -0800 Subject: [PATCH 18/19] fix local name changes --- letsencrypt/client/revoker.py | 2 +- letsencrypt/client/tests/client_authenticator_test.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/letsencrypt/client/revoker.py b/letsencrypt/client/revoker.py index 3a194610b..297866e54 100644 --- a/letsencrypt/client/revoker.py +++ b/letsencrypt/client/revoker.py @@ -23,7 +23,7 @@ class Revoker(object): """ def __init__(self, installer, config): - self.network = network.Network(config.acme_server) + self.network = network.Network(config.server) self.installer = installer self.config = config diff --git a/letsencrypt/client/tests/client_authenticator_test.py b/letsencrypt/client/tests/client_authenticator_test.py index 2f49b07d7..b2eff7d28 100644 --- a/letsencrypt/client/tests/client_authenticator_test.py +++ b/letsencrypt/client/tests/client_authenticator_test.py @@ -11,7 +11,7 @@ class PerformTest(unittest.TestCase): from letsencrypt.client.client_authenticator import ClientAuthenticator self.auth = ClientAuthenticator( - mock.MagicMock(acme_server="demo_server.org")) + mock.MagicMock(server="demo_server.org")) self.auth.rec_token.perform = mock.MagicMock( name="rec_token_perform", side_effect=gen_client_resp) @@ -51,8 +51,8 @@ class CleanupTest(unittest.TestCase): def setUp(self): from letsencrypt.client.client_authenticator import ClientAuthenticator - self.auth = ClientAuthenticator(mock.MagicMock( - acme_server="demo_server.org")) + self.auth = ClientAuthenticator( + mock.MagicMock(server="demo_server.org")) self.mock_cleanup = mock.MagicMock(name="rec_token_cleanup") self.auth.rec_token.cleanup = self.mock_cleanup From 523c59d32973a8c90953d223d320e335b582d038 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 9 Feb 2015 16:39:08 -0800 Subject: [PATCH 19/19] revert to binary redirect logic --- letsencrypt/scripts/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 784f8d691..a2a5f3c62 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -43,7 +43,9 @@ def create_parser(): help="Revert configuration N number of checkpoints.") add("-v", "--view-config-changes", action="store_true", help="View checkpoints and associated configuration changes.") - add("-r", "--redirect", type=bool, default=None, + + # TODO: resolve - assumes binary logic while client.py assumes ternary. + add("-r", "--redirect", action="store_true", help="Automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost.")