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:
@@ -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).
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
:mod:`letsencrypt.client.CONFIG`
|
||||
--------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.CONFIG
|
||||
:members:
|
||||
5
docs/api/client/configuration.rst
Normal file
5
docs/api/client/configuration.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`letsencrypt.client.configuration`
|
||||
---------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.configuration
|
||||
:members:
|
||||
5
docs/api/client/constants.rst
Normal file
5
docs/api/client/constants.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`letsencrypt.client.constants`
|
||||
-----------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.constants
|
||||
:members:
|
||||
@@ -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."""
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
52
letsencrypt/client/configuration.py
Normal file
52
letsencrypt/client/configuration.py
Normal 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)
|
||||
65
letsencrypt/client/constants.py
Normal file
65
letsencrypt/client/constants.py
Normal 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)."""
|
||||
@@ -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)
|
||||
|
||||
@@ -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`
|
||||
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
32
letsencrypt/client/tests/configuration_test.py
Normal file
32
letsencrypt/client/tests/configuration_test.py
Normal 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()
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user