1
0
mirror of https://github.com/certbot/certbot.git synced 2026-01-13 10:22:20 +03:00

Merge branch 'master' into revoker

Conflicts:
	letsencrypt/client/apache/configurator.py
	letsencrypt/client/client.py
	letsencrypt/client/crypto_util.py
	letsencrypt/client/interfaces.py
	letsencrypt/client/reverter.py
	letsencrypt/client/revoker.py
	letsencrypt/client/tests/apache/dvsni_test.py
	letsencrypt/client/tests/apache/parser_test.py
	letsencrypt/client/tests/challenge_util_test.py
	letsencrypt/scripts/main.py
This commit is contained in:
James Kasten
2015-02-10 00:12:23 -08:00
31 changed files with 617 additions and 496 deletions

View File

@@ -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).

View File

@@ -1,5 +0,0 @@
:mod:`letsencrypt.client.CONFIG`
--------------------------------
.. automodule:: letsencrypt.client.CONFIG
:members:

View File

@@ -0,0 +1,5 @@
:mod:`letsencrypt.client.configuration`
---------------------------------------
.. automodule:: letsencrypt.client.configuration
:members:

View File

@@ -0,0 +1,5 @@
:mod:`letsencrypt.client.constants`
-----------------------------------
.. automodule:: letsencrypt.client.constants
:members:

View File

@@ -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."""

View File

@@ -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("<IfModule mod_ssl.c>\n")
for line in orig_file:
new_file.write(line)
new_file.write("</IfModule>\n")
with open(avail_fp, 'r') as orig_file:
with open(ssl_fp, 'w') as new_file:
new_file.write("<IfModule mod_ssl.c>\n")
for line in orig_file:
new_file.write(line)
new_file.write("</IfModule>\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"
"</VirtualHost>\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)

View File

@@ -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 += "</IfModule>\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 ("<VirtualHost " + ips + ">\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"
"</VirtualHost>\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")

View File

@@ -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])

View File

@@ -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):

View File

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

View File

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

View File

@@ -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()

View File

@@ -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."""

View File

@@ -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)

View File

@@ -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)."""

View File

@@ -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)

View File

@@ -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`
"""

View File

@@ -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):

View File

@@ -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")

View File

@@ -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])

View File

@@ -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)

View File

@@ -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__':

View File

@@ -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."

View File

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

View File

@@ -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)

View File

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

View File

@@ -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):

View File

@@ -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()

View File

@@ -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():

View File

@@ -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)

View File

@@ -23,6 +23,7 @@ changes = read_file(os.path.join(here, 'CHANGES.rst'))
install_requires = [
'argparse',
'ConfArgParse',
'jsonschema',
'mock',
'pycrypto',