mirror of
https://github.com/certbot/certbot.git
synced 2026-05-28 04:34:11 -04:00
Merge pull request #165 from letsencrypt/installer_api
Installer API update
This commit is contained in:
commit
488859d03a
13 changed files with 278 additions and 242 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
1
setup.py
1
setup.py
|
|
@ -39,6 +39,7 @@ setup(
|
|||
'letsencrypt.client',
|
||||
'letsencrypt.client.apache',
|
||||
'letsencrypt.client.tests',
|
||||
'letsencrypt.client.tests.apache',
|
||||
'letsencrypt.scripts',
|
||||
],
|
||||
install_requires=install_requires,
|
||||
|
|
|
|||
Loading…
Reference in a new issue