mirror of
https://github.com/certbot/certbot.git
synced 2026-06-06 23:32:06 -04:00
Initial attempt at API modification and associated changes
This commit is contained in:
parent
d22ce3128c
commit
4b44befefa
8 changed files with 276 additions and 184 deletions
|
|
@ -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]"]
|
||||
|
|
|
|||
|
|
@ -127,7 +127,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
# on initialization
|
||||
self._prepare_server_https()
|
||||
|
||||
def deploy_cert(self, vhost, cert, key, cert_chain=None):
|
||||
self.enhance_func = {"redirect": self._enable_redirect}
|
||||
|
||||
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
|
||||
|
|
@ -141,17 +143,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
.. 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(
|
||||
|
|
@ -190,9 +188,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
if cert_chain:
|
||||
self.save_notes += "\tSSLCertificateChainFile %s\n" % cert_chain
|
||||
# This is a significant operation, make a checkpoint
|
||||
return self.save()
|
||||
# return self.save()
|
||||
|
||||
def choose_virtual_host(self, target_name):
|
||||
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
|
||||
|
|
@ -206,24 +204,27 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
"""
|
||||
# Allows for domain names to be associated with a virtual host
|
||||
# Client isn't using create_dn_server_assoc(self, dn, vh) yet
|
||||
for domain, vhost in self.assoc:
|
||||
if domain == target_name:
|
||||
return vhost
|
||||
if target_name in self.assoc:
|
||||
return self.assoc[target_name]
|
||||
# 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:
|
||||
|
|
@ -381,7 +382,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")
|
||||
|
||||
|
|
@ -427,6 +428,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`
|
||||
|
|
@ -517,24 +519,54 @@ 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:
|
||||
|
|
@ -544,14 +576,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
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(
|
||||
|
|
@ -559,9 +595,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
|
||||
|
|
@ -616,7 +654,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
|
||||
|
||||
|
|
@ -678,8 +718,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.
|
||||
|
||||
|
|
@ -759,21 +797,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
|
||||
|
||||
"""
|
||||
|
|
@ -865,7 +897,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)
|
||||
|
|
@ -873,8 +905,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
|
||||
|
|
@ -992,6 +1024,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'))
|
||||
|
|
@ -1005,31 +1038,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
|
||||
|
|
@ -62,10 +60,9 @@ class Client(object):
|
|||
|
||||
"""
|
||||
self.network = network.Network(server)
|
||||
self.names = names
|
||||
self.authkey = authkey
|
||||
|
||||
sanity_check_names([server] + names)
|
||||
# sanity_check_names([server] + names)
|
||||
|
||||
self.installer = installer
|
||||
|
||||
|
|
@ -73,14 +70,12 @@ class Client(object):
|
|||
self.auth_handler = auth_handler.AuthHandler(
|
||||
dv_auth, client_auth, self.network)
|
||||
|
||||
def obtain_certificate(self, csr,
|
||||
def obtain_certificate(self, domains,
|
||||
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.
|
||||
:type csr: :class:`CSR`
|
||||
:param str domains: list of domains to get a certificate
|
||||
|
||||
:param str cert_path: Full desired path to end certificate.
|
||||
:param str chain_path: Full desired path to end chain file.
|
||||
|
|
@ -90,13 +85,16 @@ class Client(object):
|
|||
|
||||
"""
|
||||
# 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
|
||||
csr = init_csr(self.authkey, domains)
|
||||
|
||||
# Retrieve certificate
|
||||
certificate_dict = self.acme_certificate(csr.data)
|
||||
|
||||
|
|
@ -166,44 +164,44 @@ 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)
|
||||
# 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)
|
||||
# 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 +212,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 +225,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 +270,49 @@ 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)
|
||||
for dom in domains:
|
||||
# TODO: change this to try/catch
|
||||
self.installer.enhance(dom, "redirect")
|
||||
|
||||
self.installer.save("Add Redirects")
|
||||
self.installer.restart()
|
||||
|
||||
# If successful, make sure redirect site is enabled
|
||||
if success:
|
||||
self.installer.enable_site(redirect_vhost)
|
||||
# if success:
|
||||
# self.installer.enable_site(redirect_vhost)
|
||||
|
||||
def get_virtual_hosts(self, domains):
|
||||
"""Retrieve the appropriate virtual host for the domain
|
||||
# def get_virtual_hosts(self, domains):
|
||||
# """Retrieve the appropriate virtual host for the domain
|
||||
|
||||
:param list domains: Domains to find ssl vhosts for
|
||||
# :param list domains: Domains to find ssl vhosts for
|
||||
|
||||
:returns: associated vhosts
|
||||
:rtype: :class:`letsencrypt.client.apache.obj.VirtualHost`
|
||||
# :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
|
||||
# """
|
||||
# vhost = set()
|
||||
# for name in domains:
|
||||
# host = self.installer.choose_virtual_host(name)
|
||||
# if host is not None:
|
||||
# vhost.add(host)
|
||||
# return vhost
|
||||
|
||||
|
||||
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 +327,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():
|
||||
|
|
|
|||
|
|
@ -53,48 +53,70 @@ 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 choose_virtual_host(name):
|
||||
"""Chooses a virtual host based on a given domain name."""
|
||||
# def choose_virtual_host(domain):
|
||||
# """Chooses a virtual host based on a given domain name."""
|
||||
|
||||
def enable_redirect(ssl_vhost):
|
||||
"""Redirect all traffic to the given ssl_vhost (port 80 => 443)."""
|
||||
# def enable_redirect(ssl_vhost):
|
||||
# """Redirect all traffic to the given ssl_vhost (port 80 => 443)."""
|
||||
|
||||
def enable_hsts(ssl_vhost):
|
||||
"""Enable HSTS on the given ssl_vhost."""
|
||||
# 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."""
|
||||
# def enable_ocsp_stapling(ssl_vhost):
|
||||
# """Enable OCSP stapling on given ssl_vhost."""
|
||||
|
||||
def enhance(domain, enhancment, options=None):
|
||||
"""Peform a configuration enhancment.
|
||||
|
||||
: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 supported_enhancements():
|
||||
"""Returns a list of supported enhancments.
|
||||
|
||||
: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 enable_site(vhost):
|
||||
# """Enable the site at the given vhost.
|
||||
|
||||
# :param vhost: domain
|
||||
|
||||
# """
|
||||
|
||||
def save(title=None, temporary=False):
|
||||
"""Saves all changes to the configuration files.
|
||||
|
|
@ -109,13 +131,14 @@ 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."""
|
||||
|
|
|
|||
|
|
@ -68,9 +68,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"),
|
||||
|
|
|
|||
|
|
@ -34,17 +34,18 @@ 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("-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 +57,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 +88,46 @@ 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))
|
||||
# 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"))
|
||||
# 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 +178,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 +214,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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue