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).
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:
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
deleted file mode 100644
index 5a07a4aa2..000000000
--- a/letsencrypt/client/CONFIG.py
+++ /dev/null
@@ -1,103 +0,0 @@
-"""Config for Let's Encrypt."""
-import os.path
-
-
-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."""
-
-# Directories
-SERVER_ROOT = "/etc/apache2/"
-"""Apache server root directory"""
-
-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(SERVER_ROOT, "keys/")
-"""Where all keys should be stored"""
-
-CERT_DIR = os.path.join(SERVER_ROOT, "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"""
-
-CERT_PATH = CERT_DIR + "cert-letsencrypt.pem"
-"""Let's Encrypt cert file."""
-
-CHAIN_PATH = 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."""
-
-APACHE2 = "/etc/init.d/apache2"
-"""Command used for reload and restart."""
diff --git a/letsencrypt/client/apache/configurator.py b/letsencrypt/client/apache/configurator.py
index 6ca26bd24..198d83e81 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,9 +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 str server_root: Path to Apache root directory
- :ivar dict location: Path to various files associated
- with the configuration
+ :ivar config: Configuration.
+ :type config: :class:`~letsencrypt.client.interfaces.IConfig`
+
:ivar tup version: version of Apache
:ivar list vhosts: All vhosts found in the configuration
(:class:`list` of :class:`letsencrypt.client.apache.obj.VirtualHost`)
@@ -75,34 +74,22 @@ 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, 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}
-
- super(ApacheConfigurator, self).__init__(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")
@@ -124,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.
@@ -330,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 = []
@@ -382,9 +369,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 +415,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 +429,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 +514,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 +551,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 +577,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",
+ 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()
@@ -630,9 +617,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(constants.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) !=
+ constants.APACHE_REWRITE_HTTPS_ARGS[idx]):
# Not a letsencrypt https rewrite
return True, 2
# Existing letsencrypt https rewrite rule is in place
@@ -681,7 +669,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"LogLevel warn\n"
"\n"
% (servername, serveralias,
- " ".join(CONFIG.REWRITE_HTTPS_ARGS)))
+ " ".join(constants.APACHE_REWRITE_HTTPS_ARGS)))
# Write out the file
# This is the default name
@@ -694,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
@@ -709,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
@@ -841,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
@@ -866,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)
@@ -876,14 +863,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 +881,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()
@@ -920,14 +907,15 @@ 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)
@classmethod
def __str__(cls):
return "Apache version %s" % ".".join(get_version())
+
###########################################################################
# Challenges Section
###########################################################################
@@ -985,7 +973,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.restart()
-def get_version():
+def get_version(apache_ctl):
"""Return version of Apache Server.
Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7))
@@ -999,13 +987,13 @@ def get_version():
"""
try:
proc = subprocess.Popen(
- [CONFIG.APACHE_CTL, '-v'],
+ [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" % apache_ctl)
regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE)
matches = regex.findall(text)
@@ -1017,47 +1005,51 @@ def get_version():
return tuple([int(i) for i in matches[0].split('.')])
-def enable_mod(mod_name):
+def enable_mod(mod_name, apache_init, 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: 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)
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
@@ -1072,9 +1064,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
@@ -1086,7 +1080,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()
@@ -1143,6 +1137,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 feb25c3eb..9b4cd957a 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
@@ -11,8 +11,9 @@ from letsencrypt.client.apache import parser
class ApacheDvsni(object):
"""Class performs DVSNI challenges within the Apache configurator.
- :ivar config: ApacheConfigurator object
- :type config: :class:`letsencrypt.client.apache.configurator`
+ :ivar configurator: ApacheConfigurator object
+ :type configurator:
+ :class:`letsencrypt.client.apache.configurator.ApacheConfigurator`
:ivar dvsni_chall: Data required for challenges.
where DvsniChall tuples have the following fields
@@ -32,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):
@@ -59,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",
@@ -74,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():
@@ -94,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
@@ -102,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)
@@ -130,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)
@@ -145,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
@@ -164,18 +167,20 @@ 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 + CONFIG.INVALID_EXT + "\n"
+ "ServerName " + nonce + constants.DVSNI_DOMAIN_SUFFIX + "\n"
"UseCanonicalName on\n"
"SSLStrictSNIVHostCheck on\n"
"\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 " + document_root + "\n"
"\n\n")
def get_cert_file(self, nonce):
@@ -187,4 +192,4 @@ class ApacheDvsni(object):
:rtype: str
"""
- return self.config.direc["work"] + 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/augeas_configurator.py b/letsencrypt/client/augeas_configurator.py
index c35df5c2d..8854fef09 100644
--- a/letsencrypt/client/augeas_configurator.py
+++ b/letsencrypt/client/augeas_configurator.py
@@ -3,35 +3,26 @@ import logging
import augeas
-from letsencrypt.client import CONFIG
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, direc=None):
- """Initialize Augeas Configurator.
-
- :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
@@ -43,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(direc)
+ self.reverter = reverter.Reverter(config)
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 e2ff9d292..6f0ece535 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
@@ -258,12 +260,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))
@@ -449,7 +451,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 d1b105d8f..2619f3f23 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 aca683265..2ec530f82 100644
--- a/letsencrypt/client/client.py
+++ b/letsencrypt/client/client.py
@@ -9,7 +9,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
@@ -39,43 +38,41 @@ 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)
- def __init__(self, server, authkey, dv_auth, installer):
+ def __init__(self, config, authkey, dv_auth, installer):
"""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`
"""
- self.network = network.Network(server)
+ self.network = network.Network(config.server)
self.authkey = authkey
-
self.installer = installer
+ self.config = config
if dv_auth is not None:
- client_auth = client_authenticator.ClientAuthenticator(server)
+ client_auth = client_authenticator.ClientAuthenticator(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`
@@ -94,16 +91,14 @@ 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)
# Save Certificate
cert_file, chain_file = self.save_certificate(
- certificate_dict, cert_path, chain_path)
-
- print cert_file
+ certificate_dict, self.config.cert_path, self.config.chain_path)
revoker.Revoker.store_cert_key(cert_file, self.authkey.file, False)
@@ -288,7 +283,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
@@ -296,19 +291,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()
@@ -316,15 +311,23 @@ def init_key(key_size):
return le_util.Key(key_filename, key_pem)
+def init_csr(privkey, names, cert_dir):
+ """Initialize a CSR with the given private key.
-def init_csr(privkey, names):
- """Initialize a CSR with the given private key."""
+ :param privkey: Key to include in the CSR
+ :type privkey: :class:`letsencrypt.client.le_util.Key`
+
+ :param list names: `str` names to include in the CSR
+
+ :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()
@@ -341,11 +344,16 @@ 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`
+
+ """
auths = []
try:
- auths.append(configurator.ApacheConfigurator())
+ auths.append(configurator.ApacheConfigurator(config))
except errors.LetsEncryptNoInstallationError:
logging.info("Unable to determine a way to authenticate the server")
if len(auths) > 1:
@@ -354,15 +362,20 @@ def determine_authenticator():
return auths[0]
-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
@@ -379,12 +392,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
@@ -395,9 +411,16 @@ def rollback(checkpoints):
installer.restart()
-def _misconfigured_rollback(checkpoints):
- """Handles the case where the Installer is misconfigured."""
- yes = zope.component.getUtility(interfaces.IDisplay).yesno(
+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`
+
+ """
+ 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 "
"configuration?".format(os.linesep))
@@ -410,13 +433,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.")
@@ -425,16 +448,17 @@ def _misconfigured_rollback(checkpoints):
"Rollback was unable to solve the misconfiguration issues")
-def revoke(server):
+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`
"""
# 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).notification(
"The web server is currently misconfigured. Some "
@@ -446,12 +470,15 @@ def revoke(server):
revoc.display_menu()
-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..7229239dc 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,15 @@ 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, 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(
+ 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
new file mode 100644
index 000000000..1bdbe2059
--- /dev/null
+++ b/letsencrypt/client/configuration.py
@@ -0,0 +1,52 @@
+"""Let's Encrypt user-supplied configuration."""
+import os
+import zope.interface
+
+from letsencrypt.client import constants
+from letsencrypt.client import interfaces
+
+
+class NamespaceConfig(object):
+ """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``
+ - ``rec_token_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):
+ self.namespace = namespace
+
+ def __getattr__(self, name):
+ return getattr(self.namespace, name)
+
+ @property
+ def temp_checkpoint_dir(self): # pylint: disable=missing-docstring
+ return os.path.join(
+ 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)
+
+ @property
+ def cert_key_backup(self): # pylint: disable=missing-docstring
+ return os.path.join(
+ self.namespace.work_dir, constants.CERT_KEY_BACKUP_DIR)
+
+ @property
+ 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
new file mode 100644
index 000000000..3652face7
--- /dev/null
+++ b/letsencrypt/client/constants.py
@@ -0,0 +1,65 @@
+"""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."""
+
+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."""
+
+
+TEMP_CHECKPOINT_DIR = "temp_checkpoint"
+"""Temporary checkpoint directory (relative to IConfig.work_dir)."""
+
+IN_PROGRESS_DIR = "IN_PROGRESS"
+"""Directory used before a permanent checkpoint is finalized (relative to
+IConfig.work_dir)."""
+
+CERT_KEY_BACKUP_DIR = "keys-certs"
+"""Directory where all certificates and keys are stored (relative to
+IConfig.work_dir. Used for easy revocation."""
+
+REC_TOKEN_DIR = "recovery_tokens"
+"""Directory where all recovery tokens are saved (relative to
+IConfig.work_dir)."""
diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py
index a2e4d27be..4e6779b5f 100644
--- a/letsencrypt/client/crypto_util.py
+++ b/letsencrypt/client/crypto_util.py
@@ -15,11 +15,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:: Protect against crypto unicode errors... is this sufficient?
@@ -30,19 +30,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 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 msg: Message to be signed
+ :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 3ceeee3e8..2530fa752 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):
@@ -69,10 +70,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.
"""
@@ -81,7 +82,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`
"""
diff --git a/letsencrypt/client/recovery_token.py b/letsencrypt/client/recovery_token.py
index 84e91e891..4d556eb51 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 b0e94c663..1e65d54ba 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 errors
from letsencrypt.client import interfaces
from letsencrypt.client import le_util
@@ -14,13 +13,14 @@ from letsencrypt.client.display import display_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}
- self.direc = direc
+ """Reverter Class - save and revert configuration checkpoints.
+
+ :param config: Configuration.
+ :type config: :class:`letsencrypt.client.interfaces.IConfig`
+
+ """
+ def __init__(self, config):
+ self.config = config
def revert_temporary_config(self):
"""Reload users original configuration files after a temporary save.
@@ -32,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")
@@ -63,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:
@@ -71,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:
@@ -88,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:
@@ -102,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())
@@ -136,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
@@ -148,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.
@@ -258,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())
@@ -299,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())
@@ -325,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
@@ -333,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.
@@ -386,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
@@ -396,13 +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):
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:
@@ -423,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
@@ -436,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 48d4deaff..dd4cbe3bd 100644
--- a/letsencrypt/client/revoker.py
+++ b/letsencrypt/client/revoker.py
@@ -8,9 +8,9 @@ import shutil
import M2Crypto
from letsencrypt.client import acme
-from letsencrypt.client import CONFIG
from letsencrypt.client import errors
from letsencrypt.client import le_util
+
from letsencrypt.client import network
from letsencrypt.client.display import display_util
@@ -26,20 +26,27 @@ class Revoker(object):
:type network: :class:`letsencrypt.client.network`
:ivar installer: Installer object
- :type installer: :class:`letsencrypt.client.interfaces.IInstaller`
+ :type installer: :class:`~letsencrypt.client.interfaces.IInstaller`
+
+ :ivar config: Configuration.
+ :type config: :class:`~letsencrypt.client.interfaces.IConfig`
"""
- list_path = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST")
- marked_path = os.path.join(CONFIG.CERT_KEY_BACKUP, "MARKED")
-
- def __init__(self, server, installer):
- self.network = network.Network(server)
+ def __init__(self, installer, config):
+ self.network = network.Network(config.server)
self.installer = installer
+ self.config = config
+
# This will go through and make sure that nothing almost got revoked...
# but didn't quite make it... also, guarantees no orphan cert/key files
self.recovery_routine()
+ # TODO: WTF do I do with these...
+ self.list_path = os.path.join(config.cert_key_backup, "LIST")
+ self.marked_path = os.path.join(config.cert_key_backup, "MARKED")
+
+
def revoke_from_interface(self, cert):
"""Handle ACME "revocation" phase.
@@ -84,9 +91,9 @@ class Revoker(object):
def recovery_routine(self):
"""Intended to make sure files aren't orphaned."""
- if not os.path.isfile(Revoker.marked_path):
+ if not os.path.isfile(self.marked_path):
return
- with open(Revoker.marked_path, "r") as marked_file:
+ with open(self.marked_path, "r") as marked_file:
csvreader = csv.reader(marked_file)
for row in csvreader:
self.revoke(row[0], row[1])
@@ -100,18 +107,18 @@ class Revoker(object):
if os.path.isfile(Revoker.marked_path):
raise errors.LetsEncryptRevokerError(
"MARKED file was never cleaned.")
- with open(Revoker.marked_path, "w") as marked_file:
+ with open(self.marked_path, "w") as marked_file:
csvwriter = csv.writer(marked_file)
csvwriter.writerow([cert.backup_path, cert.backup_key_path])
def _remove_mark(self): # pylint: disable=no-self-use
"""Remove the marked file."""
- os.remove(Revoker.marked_path)
+ os.remove(self.marked_path)
def display_menu(self):
"""List trusted Let's Encrypt certificates."""
- if not os.path.isfile(Revoker.list_path):
+ if not os.path.isfile(self.list_path):
logging.info(
"You don't have any certificates saved from letsencrypt")
return
@@ -131,14 +138,14 @@ class Revoker(object):
# pylint: disable=no-self-use
"""Populate a list of all the saved certs."""
certs = []
- with open(Revoker.list_path, "rb") as csvfile:
+ with open(self.list_path, "rb") as csvfile:
csvreader = csv.reader(csvfile)
# idx, orig_cert, orig_key
for row in csvreader:
# Generate backup key/cert names
- 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 = Cert(b_c)
@@ -191,7 +198,7 @@ class Revoker(object):
def _remove_cert_from_list(self, cert): # pylint: disable=no-self-use
"""Remove a certificate from the LIST file."""
- list_path2 = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST.tmp")
+ list_path2 = os.path.join(self.config.cert_key_backup, "LIST.tmp")
with open(Revoker.list_path, "rb") as orgfile:
csvreader = csv.reader(orgfile)
@@ -209,12 +216,14 @@ class Revoker(object):
os.remove(list_path2)
@classmethod
- def store_cert_key(cls, cert_path, key_path, encrypt=False):
+ def store_cert_key(cls, cert_path, key_path, config, encrypt=False):
"""Store certificate key. (Used to allow quick revocation)
:param str cert_path: Path to a certificate file.
:param key_path: Authorized key for certificate
:type key_path: :class:`letsencrypt.client.le_util.Key`
+ :ivar config: Configuration.
+ :type config: :class:`~letsencrypt.client.interfaces.IConfig`
:param bool encrypt: Should the certificate key be encrypted?
@@ -222,7 +231,8 @@ class Revoker(object):
:rtype: bool
"""
- le_util.make_or_verify_dir(CONFIG.CERT_KEY_BACKUP, 0o700)
+ list_path = (config.cert_key_backup, "LIST")
+ le_util.make_or_verify_dir(config.cert_key_backup, 0o700)
idx = 0
if encrypt:
@@ -232,23 +242,23 @@ class Revoker(object):
"next update!")
return False
- cls._append_index_file(cert_path, key_path)
+ cls._append_index_file(cert_path, key_path, list_path)
shutil.copy2(key_path,
os.path.join(
- CONFIG.CERT_KEY_BACKUP,
+ config.cert_key_backup,
os.path.basename(key_path) + "_" + str(idx)))
shutil.copy2(cert_path,
os.path.join(
- CONFIG.CERT_KEY_BACKUP,
+ config.cert_key_backup,
os.path.basename(cert_path) + "_" + str(idx)))
return True
@classmethod
- def _append_index_file(cls, cert_path, key_path):
- if os.path.isfile(Revoker.list_path):
- with open(Revoker.list_path, 'r+b') as csvfile:
+ def _append_index_file(cls, cert_path, key_path, list_path):
+ if os.path.isfile(list_path):
+ with open(list_path, 'r+b') as csvfile:
csvreader = csv.reader(csvfile)
# Find the highest index in the file
@@ -258,7 +268,7 @@ class Revoker(object):
csvwriter.writerow([str(idx), cert_path, key_path])
else:
- with open(Revoker.list_path, 'wb') as csvfile:
+ with open(list_path, 'wb') as csvfile:
csvwriter = csv.writer(csvfile)
csvwriter.writerow(["0", cert_path, key_path])
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 eedc32f87..3e1054389 100644
--- a/letsencrypt/client/tests/apache/dvsni_test.py
+++ b/letsencrypt/client/tests/apache/dvsni_test.py
@@ -6,9 +6,10 @@ import shutil
import mock
from letsencrypt.client import challenge_util
-from letsencrypt.client import CONFIG
+from letsencrypt.client import constants
from letsencrypt.client import le_util
+
from letsencrypt.client.tests.apache import util
@@ -100,7 +101,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 +126,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,27 +143,30 @@ 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):
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/parser_test.py b/letsencrypt/client/tests/apache/parser_test.py
index 3022940f3..3f5cc36ae 100644
--- a/letsencrypt/client/tests/apache/parser_test.py
+++ b/letsencrypt/client/tests/apache/parser_test.py
@@ -9,7 +9,6 @@ import mock
import zope.component
from letsencrypt.client import errors
-from letsencrypt.client.apache import parser
from letsencrypt.client.display import display_util
from letsencrypt.client.tests.apache import util
@@ -23,15 +22,32 @@ class ApacheParserTest(util.ApacheTest):
zope.component.provideUtility(display_util.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 fe27921b7..78566e1e4 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
@@ -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')
@@ -50,11 +49,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,15 +64,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(
- config_path,
- {
- "backup": backups,
- "temp": os.path.join(work_dir, "temp_checkpoint"),
- "progress": os.path.join(backups, "IN_PROGRESS"),
- "config": config_dir,
- "work": work_dir,
- },
- ssl_options,
+ 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/challenge_util_test.py b/letsencrypt/client/tests/challenge_util_test.py
index 54f1f13d2..97f341b0d 100644
--- a/letsencrypt/client/tests/challenge_util_test.py
+++ b/letsencrypt/client/tests/challenge_util_test.py
@@ -7,7 +7,7 @@ import unittest
import M2Crypto
from letsencrypt.client import challenge_util
-from letsencrypt.client import CONFIG
+from letsencrypt.client import constants
from letsencrypt.client import le_util
@@ -36,11 +36,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..b2eff7d28 100644
--- a/letsencrypt/client/tests/client_authenticator_test.py
+++ b/letsencrypt/client/tests/client_authenticator_test.py
@@ -6,10 +6,12 @@ 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(
+ mock.MagicMock(server="demo_server.org"))
self.auth.rec_token.perform = mock.MagicMock(
name="rec_token_perform", side_effect=gen_client_resp)
@@ -45,10 +47,12 @@ 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(
+ mock.MagicMock(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 497bb8be0..c490df770 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/configuration_test.py b/letsencrypt/client/tests/configuration_test.py
new file mode 100644
index 000000000..a07953396
--- /dev/null
+++ b/letsencrypt/client/tests/configuration_test.py
@@ -0,0 +1,32 @@
+"""Tests for letsencrypt.client.configuration."""
+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.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.rec_token_dir, '/r')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/letsencrypt/client/tests/reverter_test.py b/letsencrypt/client/tests/reverter_test.py
index 028d037bd..0e3346924 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(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(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")
@@ -349,7 +349,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)
@@ -383,29 +383,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
- rev = Reverter()
-
- # Verify direc is set
- self.assertTrue(rev.direc['backup'])
- self.assertTrue(rev.direc['temp'])
- self.assertTrue(rev.direc['progress'])
-
-
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 24892f6a8..78ea122cd 100755
--- a/letsencrypt/scripts/main.py
+++ b/letsencrypt/scripts/main.py
@@ -9,11 +9,12 @@ import logging
import os
import sys
+import confargparse
import zope.component
-import zope.interface
import letsencrypt
-from letsencrypt.client import CONFIG
+
+from letsencrypt.client import configuration
from letsencrypt.client import client
from letsencrypt.client import errors
from letsencrypt.client import interfaces
@@ -23,49 +24,74 @@ from letsencrypt.client.display import display_util
from letsencrypt.client.display import ops
-def main(): # pylint: disable=too-many-statements,too-many-branches
- """Command line argument parsing and main script execution."""
- parser = argparse.ArgumentParser(
+def create_parser():
+ """Create parser."""
+ parser = confargparse.ConfArgParser(
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
+ config_help = lambda name: interfaces.IConfig[name].__doc__
+ add("-d", "--domains", metavar="DOMAIN", nargs="+")
+ 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.")
+ 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",
+ help="Revert configuration N number of checkpoints.")
+ add("-v", "--view-config-changes", action="store_true",
+ help="View checkpoints and associated configuration changes.")
+
+ # 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.")
+
+ 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("--config-dir", default="/etc/letsencrypt",
+ help=config_help("config_dir"))
+ add("--work-dir", default="/var/lib/letsencrypt",
+ help=config_help("work_dir"))
+ add("--backup-dir", default="/var/lib/letsencrypt/backups",
+ 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=config_help("cert_dir"))
+
+ add("--le-vhost-ext", default="-le-ssl.conf",
+ help=config_help("le_vhost_ext"))
+ add("--cert-path", default="/etc/letsencrypt/certs/cert-letsencrypt.pem",
+ help=config_help("cert_path"))
+ add("--chain-path", default="/etc/letsencrypt/certs/chain-letsencrypt.pem",
+ help=config_help("chain_path"))
+
+ add("--apache-server-root", default="/etc/apache2",
+ help=config_help("apache_server_root"))
+ add("--apache-mod-ssl-conf", default="/etc/letsencrypt/options-ssl.conf",
+ 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
+
+
+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()
+ 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:
@@ -84,15 +110,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(config)
sys.exit()
if args.rollback > 0:
- client.rollback(args.rollback)
+ client.rollback(args.rollback, config)
sys.exit()
if not args.eula:
@@ -118,11 +144,11 @@ def main(): # pylint: disable=too-many-statements,too-many-branches
# Prepare for init of Client
if args.privkey is None:
- privkey = client.init_key(args.key_size)
+ privkey = client.init_key(args.rsa_key_size, config.key_dir)
else:
privkey = le_util.Key(args.privkey[0], args.privkey[1])
- acme = client.Client(args.server, privkey, auth, installer)
+ acme = client.Client(config, privkey, auth, installer)
# Validate the key and csr
client.validate_key_csr(privkey)
diff --git a/setup.py b/setup.py
index 634437558..51c3a1c27 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',