Merge pull request #165 from letsencrypt/installer_api

Installer API update
This commit is contained in:
James Kasten 2015-01-18 00:07:49 -08:00
commit 488859d03a
13 changed files with 278 additions and 242 deletions

View file

@ -322,7 +322,11 @@ max-attributes=7
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Pylint counts all of the public methods that you also inherit.
# This has been reported/fixed as a bug, but until our version is fixed,
# I think this will only cause us headaches. (Unittests are automatically over)
# https://bitbucket.org/logilab/pylint/issue/248/too-many-public-methods-triggered-from
max-public-methods=100
[EXCEPTIONS]

View file

@ -127,13 +127,9 @@ optional arguments:
-p PRIVKEY, --privkey PRIVKEY
Path to the private key file for certificate
generation.
-c CSR, --csr CSR Path to the certificate signing request file
corresponding to the private key file. The private key
file argument is required if this argument is
specified.
-b N, --rollback N Revert configuration N number of checkpoints.
-k, --revoke Revoke a certificate.
-v, --view-checkpoints
-v, --view-config-changes
View checkpoints and associated configuration changes.
-r, --redirect Automatically redirect all HTTP traffic to HTTPS for
the newly authenticated vhost.

View file

@ -78,6 +78,33 @@ NONCE_SIZE = 16
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
"""
# ENHANCEMENTS = [
# {
# "type": "redirect",
# "description": ("Please choose whether HTTPS access is required or "
# "optional."),
# "options": [
# ("Easy", "Allow both HTTP and HTTPS access to thses sites"),
# ("Secure", "Make all requests redirect to secure HTTPS access"),
# ],
# },
# {
# "type": ""
# }
# ]
# Config Optimizations
REWRITE_HTTPS_ARGS = [
"^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"]

View file

@ -49,14 +49,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
Apache 2.2 and this code works for Ubuntu 14.04 Apache 2.4. Further
notes below.
This class was originally developed for Apache 2.2 and has not seen a
an overhaul to include proper setup of new Apache configurations.
This class was originally developed for Apache 2.2 and I have been slowly
transitioning the codebase to work with all of the 2.4 features.
I have implemented most of the changes... the missing ones are
mod_ssl.c vs ssl_mod, and I need to account for configuration variables.
That being said, this class can still adequately configure most typical
Apache 2.4 servers as the deprecated NameVirtualHost has no effect
and the typical directories are parsed by the Augeas configuration
parser automatically.
This class can adequately configure most typical configurations but
is not ready to handle very complex configurations.
.. todo:: Add support for config file variables Define rootDir /var/www/
.. todo:: Add proper support for module configuration
@ -125,39 +123,35 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Enable mod_ssl if it isn't already enabled
# This is Let's Encrypt... we enable mod_ssl on initialization :)
# TODO: attempt to make the check faster... this enable should
# be asynchronous as it shouldn't be that time sensitive
# on initialization
# be asynchronous as it shouldn't be that time sensitive
# on initialization
self._prepare_server_https()
# Move temporary files before release to reduce developer
# problems.
self.enhance_func = {"redirect": self._enable_redirect}
temp_install(ssl_options)
def deploy_cert(self, vhost, cert, key, cert_chain=None):
def deploy_cert(self, domain, cert, key, cert_chain=None):
"""Deploys certificate to specified virtual host.
Currently tries to find the last directives to deploy the cert in
the given virtualhost. If it can't find the directives, it searches
the "included" confs. The function verifies that it has located
the three directives and finally modifies them to point to the correct
destination
the VHost associated with the given domain. If it can't find the
directives, it searches the "included" confs. The function verifies that
it has located the three directives and finally modifies them to point
to the correct destination. After the certificate is installed, the
VirtualHost is enabled if it isn't already.
.. todo:: Make sure last directive is changed
.. todo:: Might be nice to remove chain directive if none exists
This shouldn't happen within letsencrypt though
:param vhost: ssl vhost to deploy certificate
:type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
:param str domain: domain to deploy certificate
:param str cert: certificate filename
:param str key: private key filename
:param str cert_chain: certificate chain filename
:returns: Success
:rtype: bool
"""
vhost = self.choose_vhost(domain)
path = {}
path["cert_file"] = self.parser.find_dir(parser.case_i(
@ -195,11 +189,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.save_notes += "\tSSLCertificateKeyFile %s\n" % key
if cert_chain:
self.save_notes += "\tSSLCertificateChainFile %s\n" % cert_chain
# This is a significant operation, make a checkpoint
return self.save()
def choose_virtual_host(self, target_name):
""" Chooses a virtual host based on the given domain name.
# Make sure vhost is enabled
if not vhost.enabled:
self.enable_site(vhost)
def choose_vhost(self, target_name):
"""Chooses a virtual host based on the given domain name.
.. todo:: This should maybe return list if no obvious answer
is presented.
@ -217,18 +213,22 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Check for servernames/aliases for ssl hosts
for vhost in self.vhosts:
if vhost.ssl and target_name in vhost.names:
self.assoc[target_name] = vhost
return vhost
# Checking for domain name in vhost address
# This technique is not recommended by Apache but is technically valid
target_addr = obj.Addr((target_name, "443"))
for vhost in self.vhosts:
if target_addr in vhost.addrs:
self.assoc[target_name] = vhost
return vhost
# Check for non ssl vhosts with servernames/aliases == 'name'
for vhost in self.vhosts:
if not vhost.ssl and target_name in vhost.names:
return self.make_vhost_ssl(vhost)
vhost = self.make_vhost_ssl(vhost)
self.assoc[target_name] = vhost
return vhost
# No matches, search for the default
for vhost in self.vhosts:
@ -386,7 +386,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
is appropriately listening on port 443.
"""
if not check_ssl_loaded():
if not mod_loaded("ssl_module"):
logging.info("Loading mod_ssl into Apache Server")
enable_mod("ssl")
@ -432,6 +432,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
Duplicates vhost and adds default ssl options
New vhost will reside as (nonssl_vhost.path) + CONFIG.LE_VHOST_EXT
.. note:: This function saves the configuration
:param nonssl_vhost: Valid VH that doesn't have SSLEngine on
:type nonssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
@ -522,41 +523,75 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return ssl_vhost
def enable_redirect(self, ssl_vhost):
def supported_enhancements():
"""Returns currently supported enhancements."""
return ["redirect"]
def enhance(self, domain, enhancement, options=None):
"""Enhance configuration.
:param str domain: domain to enhance
:param str enhancement: enhancement type defined in
:class:`letsencrypt.client.CONFIG.ENHANCEMENTS
:param options: options for the enhancement
:type options: See :class:`letsencrypt.client.CONFIG.ENHANCEMENTS`
documentation for appropriate parameter.
"""
try:
return self.enhance_func[enhancement](
self.choose_vhost(domain), options)
except ValueError:
raise errors.LetsEncryptConfiguratorError(
"Unsupported enhancement: {}".format(enhancement))
except errors.LetsEncryptConfiguratorError:
logging.warn("Failed %s for %s", enhancement, domain)
def _enable_redirect(self, ssl_vhost, options):
"""Redirect all equivalent HTTP traffic to ssl_vhost.
.. todo:: This enhancement should be rewritten and will unfortunately
require lots of debugging by hand.
Adds Redirect directive to the port 80 equivalent of ssl_vhost
First the function attempts to find the vhost with equivalent
ip addresses that serves on non-ssl ports
The function then adds the directive
.. note:: This function saves the configuration
:param ssl_vhost: Destination of traffic, an ssl enabled vhost
:type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
:param options: Not currently used
:type options: Not Available
:returns: Success, general_vhost (HTTP vhost)
:rtype: (bool, :class:`letsencrypt.client.apache.obj.VirtualHost`)
"""
# TODO: Enable check to see if it is already there
# to avoid the extra restart
enable_mod("rewrite")
if not mod_loaded("rewrite_module"):
enable_mod("rewrite")
general_v = self._general_vhost(ssl_vhost)
if general_v is None:
# Add virtual_server with redirect
logging.debug(
"Did not find http version of ssl virtual host... creating")
return self.create_redirect_vhost(ssl_vhost)
return self._create_redirect_vhost(ssl_vhost)
else:
# Check if redirection already exists
exists, code = self.existing_redirect(general_v)
exists, code = self._existing_redirect(general_v)
if exists:
if code == 0:
logging.debug("Redirect already added")
return True, general_v
logging.info(
"Configuration is already redirecting traffic to HTTPS")
return
else:
logging.debug("Unknown redirect exists for this vhost")
return False, general_v
logging.info("Unknown redirect exists for this vhost")
raise errors.LetsEncryptConfiguratorError(
"Unknown redirect already exists "
"in {}".format(general_v.filep))
# Add directives to server
self.parser.add_dir(general_v.path, "RewriteEngine", "On")
self.parser.add_dir(
@ -564,9 +599,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.save_notes += ('Redirecting host in %s to ssl vhost in %s\n' %
(general_v.filep, ssl_vhost.filep))
self.save()
return True, general_v
def existing_redirect(self, vhost):
logging.info("Redirecting vhost in %s to ssl vhost in %s",
general_v.filep, ssl_vhost.filep)
def _existing_redirect(self, vhost):
"""Checks to see if existing redirect is in place.
Checks to see if virtualhost already contains a rewrite or redirect
@ -607,7 +644,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Rewrite path exists but is not a letsencrypt https rule
return True, 2
def create_redirect_vhost(self, ssl_vhost):
def _create_redirect_vhost(self, ssl_vhost):
"""Creates an http_vhost specifically to redirect for the ssl_vhost.
:param ssl_vhost: ssl vhost
@ -621,7 +658,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Make sure adding the vhost will be safe
conflict, host_or_addrs = self._conflicting_host(ssl_vhost)
if conflict:
return False, host_or_addrs
raise errors.LetsEncryptConfiguratorError(
"Unable to create a redirection vhost "
"- {}".format(host_or_addrs))
redirect_addrs = host_or_addrs
@ -683,8 +722,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
'ssl vhost %s\n' %
(new_vhost.filep, ssl_vhost.filep))
return True, new_vhost
def _conflicting_host(self, ssl_vhost):
"""Checks for conflicting HTTP vhost for ssl_vhost.
@ -764,21 +801,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return vhost
return None
# TODO: Handle this as outlined in Interfaces.
def enable_ocsp_stapling(self, ssl_vhost):
"""Enable OCSP Stapling."""
return False
def enable_hsts(self, ssl_vhost):
"""Enable HSTS."""
return False
def get_all_certs_keys(self):
"""Find all existing keys, certs from configuration.
Retrieve all certs and keys set in VirtualHosts on the Apache server
:returns: list of tuples with form [(cert, key, path)]
cert - str path to certificate file
key - str path to associated key file
path - File path to configuration file.
:rtype: list
"""
@ -870,7 +901,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
['sudo', '/usr/sbin/apache2ctl', 'configtest'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
text = proc.communicate()
stdout, stderr = proc.communicate()
except (OSError, ValueError):
logging.fatal("Unable to run /usr/sbin/apache2ctl configtest")
sys.exit(1)
@ -878,8 +909,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if proc.returncode != 0:
# Enter recovery routine...
logging.error("Configtest failed")
logging.error(text[0])
logging.error(text[1])
logging.error(stdout)
logging.error(stderr)
return False
return True
@ -997,6 +1028,7 @@ def enable_mod(mod_name):
"""
try:
# Use check_output so the command will finish before reloading
# TODO: a2enmod is debian specific...
subprocess.check_call(["sudo", "a2enmod", mod_name],
stdout=open("/dev/null", 'w'),
stderr=open("/dev/null", 'w'))
@ -1010,31 +1042,28 @@ def enable_mod(mod_name):
sys.exit(1)
def check_ssl_loaded():
def mod_loaded(module):
"""Checks to see if mod_ssl is loaded
Currently uses apache2ctl to get loaded module list
.. todo:: This function is likely fragile to versions/distros
Uses CONFIG.APACHE_CTL to get loaded module list
:returns: If ssl_module is included and active in Apache
:rtype: bool
"""
try:
# p=subprocess.check_output(['sudo', '/usr/sbin/apache2ctl', '-M'],
# stderr=open("/dev/null", 'w'))
proc = subprocess.Popen([CONFIG.APACHE_CTL, '-M'],
stdout=subprocess.PIPE,
stderr=open(
"/dev/null", 'w')).communicate()[0]
proc = subprocess.Popen(
[CONFIG.APACHE_CTL, '-M'],
stdout=subprocess.PIPE,
stderr=open("/dev/null", 'w')).communicate()[0]
except (OSError, ValueError):
logging.error(
"Error accessing %s for loaded modules!", CONFIG.APACHE_CTL)
logging.error("This may be caused by an Apache Configuration Error")
return False
if "ssl_module" in proc:
if module in proc:
return True
return False

View file

@ -64,7 +64,7 @@ class ApacheDvsni(object):
addresses = []
default_addr = "*:443"
for chall in self.dvsni_chall:
vhost = self.config.choose_virtual_host(chall.domain)
vhost = self.config.choose_vhost(chall.domain)
if vhost is None:
logging.error(
"No vhost exists with servername or alias of: %s",

View file

@ -187,7 +187,7 @@ class AugeasConfigurator(object):
self.aug.load()
def display_checkpoints(self):
def show_config_changes(self):
"""Displays all saved checkpoints.
All checkpoints are printed to the console.

View file

@ -34,8 +34,6 @@ class Client(object):
:ivar network: Network object for sending and receiving messages
:type network: :class:`letsencrypt.client.network.Network`
:ivar list names: Domain names (:class:`list` of :class:`str`).
:ivar authkey: Authorization Key
:type authkey: :class:`letsencrypt.client.client.Client.Key`
@ -52,7 +50,7 @@ class Client(object):
Key = collections.namedtuple("Key", "file pem")
CSR = collections.namedtuple("CSR", "file data form")
def __init__(self, server, names, authkey, dv_auth, installer):
def __init__(self, server, authkey, dv_auth, installer):
"""Initialize a client.
:param str server: CA server to contact
@ -61,25 +59,24 @@ class Client(object):
:type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
"""
sanity_check_names([server])
self.network = network.Network(server)
self.names = names
self.authkey = authkey
sanity_check_names([server] + names)
self.installer = installer
client_auth = client_authenticator.ClientAuthenticator(server)
self.auth_handler = auth_handler.AuthHandler(
dv_auth, client_auth, self.network)
def obtain_certificate(self, csr,
def obtain_certificate(self, domains, csr=None,
cert_path=CONFIG.CERT_PATH,
chain_path=CONFIG.CHAIN_PATH):
"""Obtains a certificate from the ACME server.
:param csr: A valid CSR in DER format for the certificate the client
intends to receive.
: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.
@ -89,14 +86,19 @@ class Client(object):
:rtype: `tuple` of `str`
"""
sanity_check_names(domains)
# Request Challenges
for name in self.names:
for name in domains:
self.auth_handler.add_chall_msg(
name, self.acme_challenge(name), self.authkey)
# Perform Challenges/Get Authorizations
self.auth_handler.get_authorizations()
# Create CSR from names
if csr is None:
csr = init_csr(self.authkey, domains)
# Retrieve certificate
certificate_dict = self.acme_certificate(csr.data)
@ -166,44 +168,37 @@ class Client(object):
return os.path.abspath(cert_file), cert_chain_abspath
def deploy_certificate(self, privkey, cert_file, chain_file):
def deploy_certificate(self, domains, privkey, cert_file, chain_file=None):
"""Install certificate
:returns: Path to a certificate file.
:rtype: str
:param list domains: list of domains to install the certificate
:param privkey: private key for certificate
:type privkey: :class:`Key`
:param str cert_file: certificate file path
:param str chain_file: chain file path
"""
# Find set of virtual hosts to deploy certificates to
vhost = self.get_virtual_hosts(self.names)
chain = None if chain_file is None else os.path.abspath(chain_file)
for host in vhost:
self.installer.deploy_cert(host,
for dom in domains:
self.installer.deploy_cert(dom,
os.path.abspath(cert_file),
os.path.abspath(privkey.file),
chain)
# Enable any vhost that was issued to, but not enabled
if not host.enabled:
logging.info("Enabling Site %s", host.filep)
self.installer.enable_site(host)
self.installer.save("Deployed Let's Encrypt Certificate")
# sites may have been enabled / final cleanup
self.installer.restart()
zope.component.getUtility(
interfaces.IDisplay).success_installation(self.names)
interfaces.IDisplay).success_installation(domains)
return vhost
def enhance_config(self, domains, redirect=None):
"""Enhance the configuration.
def optimize_config(self, vhost, redirect=None):
"""Optimize the configuration.
.. todo:: Handle multiple vhosts
:param vhost: vhost to optimize
:type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
:param list domains: list of domains to configure
:param redirect: If traffic should be forwarded from HTTP to HTTPS.
:type redirect: bool or None
@ -214,8 +209,7 @@ class Client(object):
interfaces.IDisplay).redirect_by_default()
if redirect:
self.redirect_to_ssl(vhost)
self.installer.restart()
self.redirect_to_ssl(domains)
# if self.ocsp_stapling is None:
# q = ("Would you like to protect the privacy of your users "
@ -228,7 +222,7 @@ class Client(object):
# continue
def store_cert_key(self, cert_file, encrypt=False):
"""Store certificate key.
"""Store certificate key. (Used to allow quick revocation)
:param str cert_file: Path to a certificate file.
@ -273,43 +267,31 @@ class Client(object):
return True
def redirect_to_ssl(self, vhost):
def redirect_to_ssl(self, domains):
"""Redirect all traffic from HTTP to HTTPS
:param vhost: list of ssl_vhosts
:type vhost: :class:`letsencrypt.client.interfaces.IInstaller`
"""
for ssl_vh in vhost:
success, redirect_vhost = self.installer.enable_redirect(ssl_vh)
logging.info(
"\nRedirect vhost: %s - %s ", redirect_vhost.filep, success)
# If successful, make sure redirect site is enabled
if success:
self.installer.enable_site(redirect_vhost)
for dom in domains:
try:
self.installer.enhance(dom, "redirect")
except errors.LetsEncryptConfiguratorError:
logging.warn('Unable to perform redirect for %s', dom)
def get_virtual_hosts(self, domains):
"""Retrieve the appropriate virtual host for the domain
:param list domains: Domains to find ssl vhosts for
:returns: associated vhosts
:rtype: :class:`letsencrypt.client.apache.obj.VirtualHost`
"""
vhost = set()
for name in domains:
host = self.installer.choose_virtual_host(name)
if host is not None:
vhost.add(host)
return vhost
self.installer.save("Add Redirects")
self.installer.restart()
def validate_key_csr(privkey, csr):
"""Validate CSR and key files.
def validate_key_csr(privkey, csr=None):
"""Validate Key and CSR files.
Verifies that the client key and csr arguments are valid and correspond to
one another. This does not currently check the names in the CSR.
one another. This does not currently check the names in the CSR due to
the inability to read SANs from CSRs in python crypto libraries.
If csr is left as None, only the key will be validated.
:param privkey: Key associated with CSR
:type privkey: :class:`letsencrypt.client.client.Client.Key`
@ -324,27 +306,28 @@ def validate_key_csr(privkey, csr):
# The client can eventually do things like prompt the user
# and allow the user to take more appropriate actions
if csr.form == "der":
csr_obj = M2Crypto.X509.load_request_der_string(csr.data)
csr = Client.CSR(csr.file, csr_obj.as_pem(), "der")
# If CSR is provided, it must be readable and valid.
if csr.data and not crypto_util.valid_csr(csr.data):
raise errors.LetsEncryptClientError(
"The provided CSR is not a valid CSR")
# If key is provided, it must be readable and valid.
# Key must be readable and valid.
if privkey.pem and not crypto_util.valid_privkey(privkey.pem):
raise errors.LetsEncryptClientError(
"The provided key is not a valid key")
# If CSR and key are provided, the key must be the same key used
# in the CSR.
if csr.data and privkey.pem:
if not crypto_util.csr_matches_pubkey(
csr.data, privkey.pem):
if csr:
if csr.form == "der":
csr_obj = M2Crypto.X509.load_request_der_string(csr.data)
csr = Client.CSR(csr.file, csr_obj.as_pem(), "der")
# If CSR is provided, it must be readable and valid.
if csr.data and not crypto_util.valid_csr(csr.data):
raise errors.LetsEncryptClientError(
"The key and CSR do not match")
"The provided CSR is not a valid CSR")
# If both CSR and key are provided, the key must be the same key used
# in the CSR.
if csr.data and privkey.pem:
if not crypto_util.csr_matches_pubkey(
csr.data, privkey.pem):
raise errors.LetsEncryptClientError(
"The key and CSR do not match")
def init_key():

View file

@ -1,7 +1,7 @@
"""Let's Encrypt client interfaces."""
import zope.interface
# pylint: disable=no-self-argument,no-method-argument
# pylint: disable=no-self-argument,no-method-argument,no-init
class IAuthenticator(zope.interface.Interface):
@ -53,49 +53,48 @@ class IInstaller(zope.interface.Interface):
"""Generic Let's Encrypt Installer Interface.
Represents any server that an X509 certificate can be placed.
With a focus on HTTPS optimizations.
.. todo:: All optimizations should be of the form .enable("hsts")
This will make it general towards any optimization... we should also
define a function to glean what optimizations are available.
Perhaps with text that describes the optimizations...
"""
def get_all_names():
"""Returns all names that may be authenticated."""
def deploy_cert(vhost, cert, key, cert_chain=None):
def deploy_cert(domain, cert, key, cert_chain=None):
"""Deploy certificate.
:param vhost
:param str cert: CSR
:param str key: Private key
:param str domain: domain to deploy certificate
:param str cert: certificate filename
:param str key: private key filename
"""
def enhance(domain, enhancment, options=None):
"""Peform a configuration enhancment.
def choose_virtual_host(name):
"""Chooses a virtual host based on a given domain name."""
:param str domain: domain for which to provide enhancement
:param str enhancement: An enhancement as defined in CONFIG.ENHANCEMENTS
:param options: flexible options parameter for enhancement
:type options: Check documentation of
:class:`letsencrypt.client.CONFIG.ENHANCEMENTS` for expected options
for each enhancement.
def enable_redirect(ssl_vhost):
"""Redirect all traffic to the given ssl_vhost (port 80 => 443)."""
"""
def supported_enhancements():
"""Returns a list of supported enhancments.
def enable_hsts(ssl_vhost):
"""Enable HSTS on the given ssl_vhost."""
def enable_ocsp_stapling(ssl_vhost):
"""Enable OCSP stapling on given ssl_vhost."""
:returns: supported enhancments which should be a subset of the
enhancments in :class:`letsencrypt.client.CONFIG.ENHANCEMENTS`
:rtype: `list` of `str`
"""
def get_all_certs_keys():
"""Retrieve all certs and keys set in configuration.
:returns: List of tuples with form [(cert, key, path)].
:returns: list of tuples with form [(cert, key, path)]
cert - str path to certificate file
key - str path to associated key file
path - file path to configuration file
:rtype: list
"""
def enable_site(vhost):
"""Enable the site at the given vhost."""
def save(title=None, temporary=False):
"""Saves all changes to the configuration files.
@ -109,13 +108,13 @@ class IInstaller(zope.interface.Interface):
:param bool temporary: Indicates whether the changes made will
be quickly reversed in the future (challenges)
"""
"""
def rollback_checkpoints(rollback=1):
"""Revert `rollback` number of configuration checkpoints."""
def display_checkpoints():
"""Display the saved configuration checkpoints."""
def view_config_changes():
"""Display all of the LE config changes."""
def config_test():
"""Make sure the configuration is valid."""
@ -128,47 +127,55 @@ class IDisplay(zope.interface.Interface):
"""Generic display."""
def generic_notification(message):
pass
"""Displays a string message
:param str message: Message to display
"""
def generic_menu(message, choices, input_text=""):
pass
"""Displays a generic menu.
:param str message: message to display
:param tup choices: choices formated as a `list` of `tup`
:param str input_text: instructions on how to make a selection
"""
def generic_input(message):
pass
"""Accept input from the user."""
def generic_yesno(message, yes_label="Yes", no_label="No"):
pass
"""A yes/no dialog."""
def filter_names(names):
pass
"""Allow the user to select which names they would like to activate."""
def success_installation(domains):
pass
"""Display a congratulations message for new https domains."""
def display_certs(certs):
pass
"""Display a list of certificates."""
def confirm_revocation(cert):
pass
"""Confirmation of revocation screen."""
def more_info_cert(cert):
pass
"""Print out all information for a given certificate dict."""
def redirect_by_default():
pass
"""Ask the user whether they would like to redirect to HTTPS."""
class IValidator(object):
"""Configuration validator."""
def redirect(name):
pass
"""Verify redirect to HTTPS."""
def ocsp_stapling(name):
pass
"""Verify ocsp stapling for domain."""
def https(names):
pass
"""Verifiy HTTPS is enabled for domain."""
def hsts(name):
pass
"""Verify HSTS header is enabled."""

View file

@ -6,7 +6,6 @@ import shutil
import unittest
import mock
import zope.component
from letsencrypt.client import challenge_util
from letsencrypt.client import client
@ -68,9 +67,12 @@ class TwoVhost80Test(unittest.TestCase):
self.assertTrue(self.config.is_site_enabled(self.vh_truth[3].filep))
def test_deploy_cert(self):
# Get the default 443 vhost
self.config.assoc["random.demo"] = self.vh_truth[1]
self.config.deploy_cert(
self.vh_truth[1],
"random.demo",
"example/cert.pem", "example/key.pem", "example/cert_chain.pem")
self.config.save()
loc_cert = self.config.parser.find_dir(
parser.case_i("sslcertificatefile"),

View file

@ -5,7 +5,6 @@ import unittest
import shutil
import mock
import zope.component
from letsencrypt.client import challenge_util
from letsencrypt.client import client
@ -15,7 +14,7 @@ from letsencrypt.client.tests.apache import config_util
class DvsniPerformTest(unittest.TestCase):
"""Test the ApacheDVSNI challenge."""
def setUp(self):
from letsencrypt.client.apache import dvsni
@ -60,10 +59,8 @@ class DvsniPerformTest(unittest.TestCase):
resp = self.sni.perform()
self.assertTrue(resp is None)
@mock.patch("letsencrypt.client.apache.configurator."
"ApacheConfigurator.restart")
@mock.patch("letsencrypt.client.challenge_util.dvsni_gen_cert")
def test_perform1(self, mock_dvsni_gen_cert, mock_restart):
def test_perform1(self, mock_dvsni_gen_cert):
chall = self.challs[0]
self.sni.add_chall(chall)
mock_dvsni_gen_cert.return_value = "randomS1"
@ -87,10 +84,8 @@ class DvsniPerformTest(unittest.TestCase):
self.assertEqual(len(responses), 1)
self.assertEqual(responses[0]["s"], "randomS1")
@mock.patch("letsencrypt.client.apache.configurator."
"ApacheConfigurator.restart")
@mock.patch("letsencrypt.client.challenge_util.dvsni_gen_cert")
def test_perform2(self, mock_dvsni_gen_cert, mock_restart):
def test_perform2(self, mock_dvsni_gen_cert):
for chall in self.challs:
self.sni.add_chall(chall)

View file

@ -1,3 +1,4 @@
"""Test the ClientAuthenticator dispatcher."""
import unittest
import mock
@ -43,6 +44,7 @@ class PerformTest(unittest.TestCase):
class CleanupTest(unittest.TestCase):
"""Test the Authenticator cleanup function."""
def setUp(self):
from letsencrypt.client.client_authenticator import ClientAuthenticator
@ -73,6 +75,7 @@ class CleanupTest(unittest.TestCase):
def gen_client_resp(chall):
"""Generate a dummy response."""
return "%s%s" % (type(chall).__name__, chall.domain)

View file

@ -34,17 +34,13 @@ def main():
parser.add_argument("-p", "--privkey", dest="privkey", type=read_file,
help="Path to the private key file for certificate "
"generation.")
parser.add_argument("-c", "--csr", dest="csr", type=read_file,
help="Path to the certificate signing request file "
"corresponding to the private key file. The "
"private key file argument is required if this "
"argument is specified.")
parser.add_argument("-b", "--rollback", dest="rollback", type=int,
default=0, metavar="N",
help="Revert configuration N number of checkpoints.")
parser.add_argument("-k", "--revoke", dest="revoke", action="store_true",
help="Revoke a certificate.")
parser.add_argument("-v", "--view-checkpoints", dest="view_checkpoints",
parser.add_argument("-v", "--view-config-changes",
dest="view_config_changes",
action="store_true",
help="View checkpoints and associated configuration "
"changes.")
@ -56,7 +52,7 @@ def main():
action="store_const", const=False,
help="Skip the HTTPS redirect question, allowing both "
"HTTP and HTTPS.")
parser.add_argument("-e", "--agree-eula", dest="eula", action="store_true",
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.")
@ -87,46 +83,35 @@ def main():
rollback(installer, args.rollback)
sys.exit()
if args.view_checkpoints:
view_checkpoints(installer)
if args.view_config_changes:
view_config_changes(installer)
sys.exit()
if not args.eula:
display_eula()
# Use the same object if possible
if interfaces.IAuthenticator.providedBy(installer):
auth = installer
else:
auth = determine_authenticator()
if not args.eula:
display_eula()
domains = choose_names(installer) if args.domains is None else args.domains
# Enforce '--privkey' is set along with '--csr'.
if args.csr and not args.privkey:
parser.error("private key file (--privkey) must be specified along{0} "
"with the certificate signing request file (--csr)"
.format(os.linesep))
# Prepare for init of Client
if args.privkey is None:
privkey = client.init_key()
else:
privkey = client.Client.Key(args.privkey[0], args.privkey[1])
if args.csr is None:
csr = client.init_csr(privkey, domains)
else:
csr = client.csr_pem_to_der(
client.Client.CSR(args.csr[0], args.csr[1], "pem"))
acme = client.Client(server, domains, privkey, auth, installer)
acme = client.Client(server, privkey, auth, installer)
# Validate the key and csr
client.validate_key_csr(privkey, csr)
client.validate_key_csr(privkey)
cert_file, chain_file = acme.obtain_certificate(csr)
vhost = acme.deploy_certificate(privkey, cert_file, chain_file)
acme.optimize_config(vhost, args.redirect)
cert_file, chain_file = acme.obtain_certificate(domains)
acme.deploy_certificate(domains, privkey, cert_file, chain_file)
acme.enhance_config(domains, args.redirect)
def display_eula():
@ -177,17 +162,21 @@ def get_all_names(installer):
# This should be controlled by commandline parameters
def determine_authenticator():
"""Returns a valid authenticator."""
"""Returns a valid IAuthenticator."""
try:
return configurator.ApacheConfigurator()
if interfaces.IAuthenticator.implementedBy(
configurator.ApacheConfigurator):
return configurator.ApacheConfigurator()
except errors.LetsEncryptConfiguratorError:
logging.info("Unable to find a way to authenticate.")
logging.info("Unable to determine a way to authenticate the server")
def determine_installer():
"""Returns a valid installer if one exists."""
try:
return configurator.ApacheConfigurator()
if interfaces.IInstaller.implementedBy(
configurator.ApacheConfigurator):
return configurator.ApacheConfigurator()
except errors.LetsEncryptConfiguratorError:
logging.info("Unable to find a way to install the certificate.")
@ -209,27 +198,27 @@ def read_file(filename):
raise argparse.ArgumentTypeError(exc.strerror)
def rollback(config, checkpoints):
def rollback(installer, checkpoints):
"""Revert configuration the specified number of checkpoints.
:param config: Configurator object
:type config: :class:`ApacheConfigurator`
:param installer: Installer object
:type installer: :class:`letsencrypt.client.interfaces.IInstaller`
:param int checkpoints: Number of checkpoints to revert.
"""
config.rollback_checkpoints(checkpoints)
config.restart()
installer.rollback_checkpoints(checkpoints)
installer.restart()
def view_checkpoints(config):
def view_config_changes(installer):
"""View checkpoints and associated configuration changes.
:param config: Configurator object
:type config: :class:`ApacheConfigurator`
:param installer: Installer object
:type installer: :class:`letsencrypt.client.interfaces.IInstaller`
"""
config.display_checkpoints()
installer.view_config_changes()
if __name__ == "__main__":
main()

View file

@ -39,6 +39,7 @@ setup(
'letsencrypt.client',
'letsencrypt.client.apache',
'letsencrypt.client.tests',
'letsencrypt.client.tests.apache',
'letsencrypt.scripts',
],
install_requires=install_requires,