diff --git a/.pylintrc b/.pylintrc index 41a75ffac..2af1870c4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -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] diff --git a/README.md b/README.md index a3a433295..9f5113931 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/letsencrypt/client/CONFIG.py b/letsencrypt/client/CONFIG.py index 2ce39a73b..7d0b581fb 100644 --- a/letsencrypt/client/CONFIG.py +++ b/letsencrypt/client/CONFIG.py @@ -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]"] diff --git a/letsencrypt/client/apache/configurator.py b/letsencrypt/client/apache/configurator.py index 085072568..c9c64da93 100644 --- a/letsencrypt/client/apache/configurator.py +++ b/letsencrypt/client/apache/configurator.py @@ -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 diff --git a/letsencrypt/client/apache/dvsni.py b/letsencrypt/client/apache/dvsni.py index c0aa552ad..b513275da 100644 --- a/letsencrypt/client/apache/dvsni.py +++ b/letsencrypt/client/apache/dvsni.py @@ -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", diff --git a/letsencrypt/client/augeas_configurator.py b/letsencrypt/client/augeas_configurator.py index 4d1caf61d..1c366c60e 100644 --- a/letsencrypt/client/augeas_configurator.py +++ b/letsencrypt/client/augeas_configurator.py @@ -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. diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 117dd143e..dd4e23c6e 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -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(): diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index be3c6e09f..b052d6ac7 100644 --- a/letsencrypt/client/interfaces.py +++ b/letsencrypt/client/interfaces.py @@ -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.""" diff --git a/letsencrypt/client/tests/apache/configurator_test.py b/letsencrypt/client/tests/apache/configurator_test.py index ce12a137e..e34fca832 100644 --- a/letsencrypt/client/tests/apache/configurator_test.py +++ b/letsencrypt/client/tests/apache/configurator_test.py @@ -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"), diff --git a/letsencrypt/client/tests/apache/dvsni_test.py b/letsencrypt/client/tests/apache/dvsni_test.py index b24d47b45..bd6838869 100644 --- a/letsencrypt/client/tests/apache/dvsni_test.py +++ b/letsencrypt/client/tests/apache/dvsni_test.py @@ -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) diff --git a/letsencrypt/client/tests/client_authenticator_test.py b/letsencrypt/client/tests/client_authenticator_test.py index 6027e1dba..9c9f4d89f 100644 --- a/letsencrypt/client/tests/client_authenticator_test.py +++ b/letsencrypt/client/tests/client_authenticator_test.py @@ -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) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 12db6e33d..ff3c3c792 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -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() diff --git a/setup.py b/setup.py index c356e0841..6c49c2f2e 100755 --- a/setup.py +++ b/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,